lineadraw 0.4.0 → 0.5.0

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/bin/lineadraw.mjs CHANGED
@@ -11,8 +11,9 @@ import {
11
11
  manifestJson,
12
12
  MANIFEST_PATH,
13
13
  verifyWithSdk,
14
+ withThumbnails,
14
15
  } from "../lib/manifest.mjs";
15
- import { init } from "../lib/init.mjs";
16
+ import { init, tsconfigLibWarning } from "../lib/init.mjs";
16
17
 
17
18
  const USAGE = `Usage: lineadraw <manifest|check|init> [--dir <path>]
18
19
 
@@ -21,7 +22,9 @@ const USAGE = `Usage: lineadraw <manifest|check|init> [--dir <path>]
21
22
  init Add the marketplace structure to the current npm project
22
23
 
23
24
  With @lineadraw/sdk installed (devDependency), manifest and check also
24
- dry-run every block. The manifest content is identical either way.`;
25
+ dry-run every block and bake preview thumbnails into the catalog — commit
26
+ the manifest from an environment with the sdk installed (the CI template
27
+ does) so check compares like with like.`;
25
28
 
26
29
  const args = process.argv.slice(2);
27
30
  const command = args[0];
@@ -40,16 +43,21 @@ const reportProblems = (problems) => {
40
43
 
41
44
  /** Validate everything (light + optional sdk dry-run); exits on failure. */
42
45
  const validatedManifest = async () => {
43
- const { manifest, sources, problems } = buildManifest(dir);
44
- const sdkResult = await verifyWithSdk(dir, manifest, sources);
46
+ const { manifest: base, sources, problems } = buildManifest(dir);
47
+ const sdkResult = await verifyWithSdk(dir, base, sources);
45
48
  const all = [...problems, ...sdkResult.problems];
46
49
  if (all.length > 0) {
47
50
  reportProblems(all);
48
51
  fail(`${all.length} problem(s) — nothing written.`);
49
52
  }
53
+ const manifest = withThumbnails(base, sdkResult.thumbnails);
50
54
  if (!sdkResult.ran)
51
55
  console.warn(
52
- "The contents of blocks/commands were not dry-run because @lineadraw/sdk is not installed. Install it as a devDependency to enable this check.",
56
+ sdkResult.reason === "load-failed"
57
+ ? `Blocks were not dry-run: @lineadraw/sdk is installed but failed to load (${sdkResult.detail}). Try reinstalling it.`
58
+ : sdkResult.reason === "too-old"
59
+ ? "Blocks were not dry-run: the installed @lineadraw/sdk is too old for module validation. Update it to enable this check."
60
+ : "Blocks were not dry-run because @lineadraw/sdk is not installed. Install it as a devDependency to enable this check.",
53
61
  );
54
62
  return manifest;
55
63
  };
@@ -75,6 +83,8 @@ switch (command) {
75
83
  fail(
76
84
  `${MANIFEST_PATH} is ${committed === null ? "missing" : "stale"} — run \`npx lineadraw manifest\` and commit the result.`,
77
85
  );
86
+ const libWarning = tsconfigLibWarning(dir);
87
+ if (libWarning !== null) console.warn(`warning: ${libWarning}`);
78
88
  console.log(
79
89
  `OK: ${manifest.entries.length} entries, manifest is current.`,
80
90
  );
@@ -82,9 +92,10 @@ switch (command) {
82
92
  }
83
93
  case "init": {
84
94
  try {
85
- const { created, skipped } = init(dir);
95
+ const { created, skipped, warnings } = await init(dir);
86
96
  for (const f of created) console.log(`created ${f}`);
87
97
  for (const f of skipped) console.log(`kept ${f}`);
98
+ for (const w of warnings) console.warn(`warning ${w}`);
88
99
  console.log(
89
100
  "\nDone. Commit, push, then add the repo in the editor: Library → Marketplace.",
90
101
  );
package/lib/init.mjs CHANGED
@@ -12,7 +12,13 @@ import {
12
12
  } from "node:fs";
13
13
  import { dirname, join } from "node:path";
14
14
  import { fileURLToPath } from "node:url";
15
- import { buildManifest, manifestJson, MANIFEST_PATH } from "./manifest.mjs";
15
+ import {
16
+ buildManifest,
17
+ manifestJson,
18
+ MANIFEST_PATH,
19
+ verifyWithSdk,
20
+ withThumbnails,
21
+ } from "./manifest.mjs";
16
22
 
17
23
  const templatesDir = join(
18
24
  dirname(fileURLToPath(import.meta.url)),
@@ -33,8 +39,29 @@ export const collectionSlug = (pkgName) => {
33
39
  return slug === "" ? "my-collection" : slug;
34
40
  };
35
41
 
36
- /** Run init in `dir`. Returns `{ created, skipped }` path lists. */
37
- export const init = (dir) => {
42
+ /**
43
+ * Warn when an existing tsconfig lets TypeScript default `lib` to include
44
+ * DOM: lib.dom's globals collide with lineadraw's global DTO aliases —
45
+ * DOM's `interface Text` (a Node) silently wins over lineadraw's `Text`
46
+ * (skipLibCheck hides the duplicate-identifier error). Repos scaffolded
47
+ * before lineadraw 0.3.1 carry such a tsconfig. Cheap string test on
48
+ * purpose: tsconfig is JSONC, so JSON.parse can't be trusted here.
49
+ * Returns the warning text, or null when the config is fine or absent.
50
+ */
51
+ export const tsconfigLibWarning = (dir) => {
52
+ const p = join(dir, "tsconfig.json");
53
+ if (!existsSync(p)) return null;
54
+ const text = readFileSync(p, "utf8");
55
+ if (/"lib"\s*:/.test(text)) return null;
56
+ return (
57
+ 'tsconfig.json has no "lib" — TypeScript then includes DOM types, and ' +
58
+ "DOM's global Text/Image/... shadow the lineadraw script types " +
59
+ '(Text becomes a DOM Node). Add to compilerOptions: "lib": ["ES2022"], "types": []'
60
+ );
61
+ };
62
+
63
+ /** Run init in `dir`. Returns `{ created, skipped, warnings }` lists. */
64
+ export const init = async (dir) => {
38
65
  const pkgPath = join(dir, "package.json");
39
66
  if (!existsSync(pkgPath))
40
67
  throw new Error(
@@ -112,16 +139,26 @@ export const init = (dir) => {
112
139
  write(".gitignore", "node_modules/\n");
113
140
 
114
141
  // Generate the catalog last, over whatever modules now exist.
115
- const { manifest, problems } = buildManifest(dir);
142
+ const { manifest: base, sources, problems } = buildManifest(dir);
116
143
  if (problems.length > 0) {
117
144
  const detail = problems
118
145
  .map((p) => `${p.file}: ${p.problems.join("; ")}`)
119
146
  .join("\n");
120
147
  throw new Error(`existing modules failed validation:\n${detail}`);
121
148
  }
149
+ // Bake thumbnails when the sdk is installed, so the very first commit's
150
+ // manifest matches what CI's `check` (sdk installed) regenerates.
151
+ const sdkResult = await verifyWithSdk(dir, base, sources);
152
+ const manifest = withThumbnails(base, sdkResult.thumbnails);
122
153
  mkdirSync(join(dir, dirname(MANIFEST_PATH)), { recursive: true });
123
154
  writeFileSync(join(dir, MANIFEST_PATH), manifestJson(manifest));
124
155
  created.push(`${MANIFEST_PATH} (${manifest.entries.length} entries)`);
125
156
 
126
- return { created, skipped };
157
+ // A kept (pre-existing) tsconfig may carry the DOM-collision footgun the
158
+ // template avoids — surface it instead of silently leaving it broken.
159
+ const warnings = [];
160
+ const libWarning = tsconfigLibWarning(dir);
161
+ if (libWarning !== null) warnings.push(libWarning);
162
+
163
+ return { created, skipped, warnings };
127
164
  };
package/lib/manifest.mjs CHANGED
@@ -5,7 +5,7 @@
5
5
  // bytes, so `check` is a plain diff.
6
6
  import { createRequire } from "node:module";
7
7
  import { existsSync, readFileSync, readdirSync } from "node:fs";
8
- import { join } from "node:path";
8
+ import { dirname, join, resolve } from "node:path";
9
9
  import { pathToFileURL } from "node:url";
10
10
 
11
11
  // sucrase is this package's own dependency — the light path needs nothing
@@ -202,24 +202,96 @@ export const MANIFEST_PATH = ".lineadraw/marketplace.json";
202
202
  * `{ ran, problems }` — `ran: false` when the sdk (or a new enough one) is
203
203
  * absent; validation-only, so manifest CONTENT never depends on it.
204
204
  */
205
- export const verifyWithSdk = async (repoDir, manifest, sources) => {
206
- let sdk;
205
+ /**
206
+ * Locate the repo's installed @lineadraw/sdk entry module. require.resolve
207
+ * alone is NOT enough: sdk ≤ 0.11 exports only the `import` condition,
208
+ * which a CJS resolver refuses (ERR_PACKAGE_PATH_NOT_EXPORTED) even though
209
+ * the package is installed — that mislabeled installed sdks as missing.
210
+ * On any resolve failure, fall back to walking node_modules by hand (the
211
+ * same directory climb Node performs) and reading the entry from the sdk's
212
+ * own package.json. Returns an absolute path, or null when truly absent.
213
+ */
214
+ const resolveSdkEntry = (repoDir) => {
207
215
  try {
208
216
  const repoRequire = createRequire(join(repoDir, "package.json"));
209
- sdk = await import(
210
- pathToFileURL(repoRequire.resolve("@lineadraw/sdk")).href
211
- );
217
+ return repoRequire.resolve("@lineadraw/sdk");
212
218
  } catch {
213
- return { ran: false, problems: [] };
219
+ for (let d = resolve(repoDir); ; d = dirname(d)) {
220
+ const pkgPath = join(d, "node_modules", "@lineadraw", "sdk", "package.json");
221
+ if (existsSync(pkgPath)) {
222
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
223
+ const dot = pkg.exports?.["."];
224
+ const entry =
225
+ (typeof dot === "string" ? dot : (dot?.import ?? dot?.default)) ??
226
+ pkg.main ??
227
+ "./dist/index.js";
228
+ return join(dirname(pkgPath), entry);
229
+ }
230
+ if (dirname(d) === d) return null;
231
+ }
232
+ }
233
+ };
234
+
235
+ export const verifyWithSdk = async (repoDir, manifest, sources) => {
236
+ const entryPath = resolveSdkEntry(repoDir);
237
+ if (entryPath === null)
238
+ return { ran: false, problems: [], reason: "not-installed" };
239
+ let sdk;
240
+ try {
241
+ sdk = await import(pathToFileURL(entryPath).href);
242
+ } catch (e) {
243
+ // Installed but not loadable (broken install, incompatible Node, …) —
244
+ // must never be reported as "not installed".
245
+ return {
246
+ ran: false,
247
+ problems: [],
248
+ reason: "load-failed",
249
+ detail: e instanceof Error ? e.message : String(e),
250
+ };
214
251
  }
215
252
  if (typeof sdk.testBlockDefinition !== "function")
216
- return { ran: false, problems: [] }; // sdk predates module validation
253
+ return { ran: false, problems: [], reason: "too-old" };
217
254
  const problems = [];
255
+ // All block sources double as the resolution set for thumbnails, so a
256
+ // block emitting instances of its catalog siblings previews complete.
257
+ const blockSources = manifest.entries
258
+ .filter((e) => e.kind === "block")
259
+ .map((e) => sources.get(e.id));
260
+ const thumbnails = new Map();
218
261
  for (const entry of manifest.entries) {
219
262
  if (entry.kind !== "block") continue; // commands are interactive — no dry run
220
263
  const result = await sdk.testBlockDefinition(sources.get(entry.id));
221
- if (!result.ok)
264
+ if (!result.ok) {
222
265
  problems.push({ file: entry.source, problems: [result.error] });
266
+ continue;
267
+ }
268
+ // Bake the preview thumbnail (sdk ≥ 0.12; older sdks still validate).
269
+ // The render is deterministic, so the manifest stays byte-stable.
270
+ if (typeof sdk.renderBlockThumbnailSvg === "function") {
271
+ try {
272
+ const svg = await sdk.renderBlockThumbnailSvg(sources.get(entry.id), {
273
+ definitions: blockSources,
274
+ });
275
+ if (typeof svg === "string") thumbnails.set(entry.id, svg);
276
+ } catch {
277
+ // A block that validates but can't render just ships no thumbnail.
278
+ }
279
+ }
223
280
  }
224
- return { ran: true, problems };
281
+ return { ran: true, problems, thumbnails };
282
+ };
283
+
284
+ /**
285
+ * Manifest with `thumbnailSvg` baked onto the block entries that have one —
286
+ * appended as the LAST key of each entry so serialization stays stable.
287
+ */
288
+ export const withThumbnails = (manifest, thumbnails) => {
289
+ if (!thumbnails || thumbnails.size === 0) return manifest;
290
+ return {
291
+ ...manifest,
292
+ entries: manifest.entries.map((entry) => {
293
+ const svg = thumbnails.get(entry.id);
294
+ return svg === undefined ? entry : { ...entry, thumbnailSvg: svg };
295
+ }),
296
+ };
225
297
  };
package/lineadraw.d.ts CHANGED
@@ -86,6 +86,12 @@ declare module "lineadraw" {
86
86
  kind: "lines";
87
87
  angle: number;
88
88
  spacing: number;
89
+ } | {
90
+ kind: "gradient";
91
+ shape?: "linear" | "radial";
92
+ color1?: string;
93
+ color2?: string;
94
+ angle?: number;
89
95
  };
90
96
  /** Pattern spacing multiplier. */
91
97
  scale?: number;
@@ -219,6 +225,12 @@ declare module "lineadraw" {
219
225
  kind: "lines";
220
226
  angle: number;
221
227
  spacing: number;
228
+ } | {
229
+ kind: "gradient";
230
+ shape: "linear" | "radial";
231
+ color1: string;
232
+ color2: string;
233
+ angle: number;
222
234
  };
223
235
  export type StoredHatch = StoredShape<{
224
236
  type: "hatch";
@@ -345,6 +357,14 @@ declare module "lineadraw" {
345
357
  type: "enum";
346
358
  default: string;
347
359
  options: readonly ParameterOption[];
360
+ }
361
+ /** A section label between rows in the properties panel. Carries no
362
+ * value: it never appears in the `params` record `draw` receives.
363
+ * `name` stays the unique key (paramVisibility can hide it by name). */
364
+ | {
365
+ name: string;
366
+ label?: string;
367
+ type: "label";
348
368
  };
349
369
  /**
350
370
  * The block's parameters table — what the `params` member of a BlockSpec
@@ -375,7 +395,9 @@ declare module "lineadraw" {
375
395
  * passes through unchanged.
376
396
  */
377
397
  export type ParamValues<P> = P extends () => infer R ? ParamValues<R> : P extends readonly (infer Q extends ParamDef)[] ? {
378
- readonly [E in Q as E["name"]]: ParamValue<E>;
398
+ readonly [E in Q as E extends {
399
+ type: "label";
400
+ } ? never : E["name"]]: ParamValue<E>;
379
401
  } : P;
380
402
 
381
403
  // ——— Script contract ———
@@ -865,6 +887,12 @@ type Hatch = Styled<{
865
887
  kind: "lines";
866
888
  angle: number;
867
889
  spacing: number;
890
+ } | {
891
+ kind: "gradient";
892
+ shape?: "linear" | "radial";
893
+ color1?: string;
894
+ color2?: string;
895
+ angle?: number;
868
896
  };
869
897
  /** Pattern spacing multiplier. */
870
898
  scale?: number;
@@ -998,6 +1026,12 @@ type StoredHatchFill = {
998
1026
  kind: "lines";
999
1027
  angle: number;
1000
1028
  spacing: number;
1029
+ } | {
1030
+ kind: "gradient";
1031
+ shape: "linear" | "radial";
1032
+ color1: string;
1033
+ color2: string;
1034
+ angle: number;
1001
1035
  };
1002
1036
  type StoredHatch = StoredShape<{
1003
1037
  type: "hatch";
@@ -1124,6 +1158,14 @@ type ParamDef = {
1124
1158
  type: "enum";
1125
1159
  default: string;
1126
1160
  options: readonly ParameterOption[];
1161
+ }
1162
+ /** A section label between rows in the properties panel. Carries no
1163
+ * value: it never appears in the `params` record `draw` receives.
1164
+ * `name` stays the unique key (paramVisibility can hide it by name). */
1165
+ | {
1166
+ name: string;
1167
+ label?: string;
1168
+ type: "label";
1127
1169
  };
1128
1170
  /**
1129
1171
  * The block's parameters table — what the `params` member of a BlockSpec
@@ -1154,7 +1196,9 @@ type ParamValue<Q extends ParamDef> = Q extends {
1154
1196
  * passes through unchanged.
1155
1197
  */
1156
1198
  type ParamValues<P> = P extends () => infer R ? ParamValues<R> : P extends readonly (infer Q extends ParamDef)[] ? {
1157
- readonly [E in Q as E["name"]]: ParamValue<E>;
1199
+ readonly [E in Q as E extends {
1200
+ type: "label";
1201
+ } ? never : E["name"]]: ParamValue<E>;
1158
1202
  } : P;
1159
1203
 
1160
1204
  // Script-contract globals: aliases into the module (not re-declarations),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lineadraw",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Type definitions and marketplace tooling for Linea blocks and commands",
5
5
  "license": "MIT",
6
6
  "author": "Mikhail Koshelev",
@@ -27,9 +27,9 @@
27
27
  "test": "vitest run"
28
28
  },
29
29
  "dependencies": {
30
- "sucrase": "^3.35.0"
30
+ "sucrase": "^3.35.1"
31
31
  },
32
32
  "devDependencies": {
33
- "vitest": "^4.1.10"
33
+ "vitest": "^4.1.11"
34
34
  }
35
35
  }
@@ -1,11 +1,18 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
+ "lib": [
5
+ "ES2022"
6
+ ],
7
+ "types": [],
4
8
  "module": "ESNext",
5
9
  "moduleResolution": "bundler",
6
10
  "strict": true,
7
11
  "noEmit": true,
8
12
  "skipLibCheck": true
9
13
  },
10
- "include": ["blocks", "commands"]
11
- }
14
+ "include": [
15
+ "blocks",
16
+ "commands"
17
+ ]
18
+ }