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 +17 -6
- package/lib/init.mjs +42 -5
- package/lib/manifest.mjs +82 -10
- package/lineadraw.d.ts +46 -2
- package/package.json +3 -3
- package/templates/tsconfig.json +9 -2
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
|
|
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,
|
|
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
|
-
|
|
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 {
|
|
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
|
-
/**
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
210
|
-
pathToFileURL(repoRequire.resolve("@lineadraw/sdk")).href
|
|
211
|
-
);
|
|
217
|
+
return repoRequire.resolve("@lineadraw/sdk");
|
|
212
218
|
} catch {
|
|
213
|
-
|
|
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: [] };
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
30
|
+
"sucrase": "^3.35.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
|
-
"vitest": "^4.1.
|
|
33
|
+
"vitest": "^4.1.11"
|
|
34
34
|
}
|
|
35
35
|
}
|
package/templates/tsconfig.json
CHANGED
|
@@ -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": [
|
|
11
|
-
|
|
14
|
+
"include": [
|
|
15
|
+
"blocks",
|
|
16
|
+
"commands"
|
|
17
|
+
]
|
|
18
|
+
}
|