lineadraw 0.4.0 → 0.6.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/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 now, so the very first commit's manifest matches what
150
+ // CI's `check` regenerates.
151
+ const sdkResult = await verifyWithSdk(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
@@ -6,7 +6,6 @@
6
6
  import { createRequire } from "node:module";
7
7
  import { existsSync, readFileSync, readdirSync } from "node:fs";
8
8
  import { join } from "node:path";
9
- import { pathToFileURL } from "node:url";
10
9
 
11
10
  // sucrase is this package's own dependency — the light path needs nothing
12
11
  // else installed.
@@ -19,7 +18,9 @@ const SEMVER_RE = /^\d+\.\d+\.\d+$/;
19
18
  export const readConfig = (repoDir) => {
20
19
  const pkgPath = join(repoDir, "package.json");
21
20
  if (!existsSync(pkgPath))
22
- throw new Error(`no package.json in ${repoDir} — run \`lineadraw init\` in an npm project`);
21
+ throw new Error(
22
+ `no package.json in ${repoDir} — run \`lineadraw init\` in an npm project`,
23
+ );
23
24
  const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
24
25
  const cfg = pkg.lineadraw ?? {};
25
26
  return {
@@ -142,17 +143,17 @@ export const buildManifest = (repoDir) => {
142
143
  entryOf("command", repoDir, cfg.commandsDir, f),
143
144
  ),
144
145
  ];
145
- const problems = results.filter((r) => r.problems).map((r) => ({
146
- file: r.file,
147
- problems: r.problems,
148
- }));
146
+ const problems = results
147
+ .filter((r) => r.problems)
148
+ .map((r) => ({
149
+ file: r.file,
150
+ problems: r.problems,
151
+ }));
149
152
  const entries = results.filter((r) => r.entry).map((r) => r.entry);
150
153
 
151
154
  const dupes = [
152
155
  ...new Set(
153
- entries
154
- .map((e) => e.id)
155
- .filter((id, i, all) => all.indexOf(id) !== i),
156
+ entries.map((e) => e.id).filter((id, i, all) => all.indexOf(id) !== i),
156
157
  ),
157
158
  ];
158
159
  if (dupes.length > 0)
@@ -167,9 +168,7 @@ export const buildManifest = (repoDir) => {
167
168
  entries.filter((e) => e.kind === "block").map((e) => e.id),
168
169
  );
169
170
  const dangling = entries.flatMap((e) =>
170
- e.dependencies
171
- .filter((d) => !blockIds.has(d))
172
- .map((d) => `${e.id} → ${d}`),
171
+ e.dependencies.filter((d) => !blockIds.has(d)).map((d) => `${e.id} → ${d}`),
173
172
  );
174
173
  if (dangling.length > 0)
175
174
  problems.push({
@@ -197,29 +196,68 @@ export const manifestJson = (manifest) =>
197
196
  export const MANIFEST_PATH = ".lineadraw/marketplace.json";
198
197
 
199
198
  /**
200
- * Optional execution-level verification: dry-run every block through
201
- * @lineadraw/sdk when the repo has it installed. Returns
202
- * `{ ran, problems }` — `ran: false` when the sdk (or a new enough one) is
203
- * absent; validation-only, so manifest CONTENT never depends on it.
199
+ * Execution-level verification: dry-run every block through this package's
200
+ * own SDK (dist/sdk.js) and bake preview thumbnails. Returns
201
+ * `{ ran, problems, thumbnails }` — `ran: false` (reason "load-failed") when
202
+ * the SDK can't load (an unbuilt checkout, a broken native install);
203
+ * validation-only, so the manifest's base CONTENT never depends on it.
204
+ * `loadSdk` is injectable for tests.
204
205
  */
205
- export const verifyWithSdk = async (repoDir, manifest, sources) => {
206
+ export const verifyWithSdk = async (
207
+ manifest,
208
+ sources,
209
+ loadSdk = () => import("../dist/sdk.js"),
210
+ ) => {
206
211
  let sdk;
207
212
  try {
208
- const repoRequire = createRequire(join(repoDir, "package.json"));
209
- sdk = await import(
210
- pathToFileURL(repoRequire.resolve("@lineadraw/sdk")).href
211
- );
212
- } catch {
213
- return { ran: false, problems: [] };
213
+ sdk = await loadSdk();
214
+ } catch (e) {
215
+ return {
216
+ ran: false,
217
+ problems: [],
218
+ reason: "load-failed",
219
+ detail: e instanceof Error ? e.message : String(e),
220
+ };
214
221
  }
215
- if (typeof sdk.testBlockDefinition !== "function")
216
- return { ran: false, problems: [] }; // sdk predates module validation
217
222
  const problems = [];
223
+ // All block sources double as the resolution set for thumbnails, so a
224
+ // block emitting instances of its catalog siblings previews complete.
225
+ const blockSources = manifest.entries
226
+ .filter((e) => e.kind === "block")
227
+ .map((e) => sources.get(e.id));
228
+ const thumbnails = new Map();
218
229
  for (const entry of manifest.entries) {
219
230
  if (entry.kind !== "block") continue; // commands are interactive — no dry run
220
231
  const result = await sdk.testBlockDefinition(sources.get(entry.id));
221
- if (!result.ok)
232
+ if (!result.ok) {
222
233
  problems.push({ file: entry.source, problems: [result.error] });
234
+ continue;
235
+ }
236
+ // Bake the preview thumbnail. The render is deterministic, so the
237
+ // manifest stays byte-stable.
238
+ try {
239
+ const svg = await sdk.renderBlockThumbnailSvg(sources.get(entry.id), {
240
+ definitions: blockSources,
241
+ });
242
+ if (typeof svg === "string") thumbnails.set(entry.id, svg);
243
+ } catch {
244
+ // A block that validates but can't render just ships no thumbnail.
245
+ }
223
246
  }
224
- return { ran: true, problems };
247
+ return { ran: true, problems, thumbnails };
248
+ };
249
+
250
+ /**
251
+ * Manifest with `thumbnailSvg` baked onto the block entries that have one —
252
+ * appended as the LAST key of each entry so serialization stays stable.
253
+ */
254
+ export const withThumbnails = (manifest, thumbnails) => {
255
+ if (!thumbnails || thumbnails.size === 0) return manifest;
256
+ return {
257
+ ...manifest,
258
+ entries: manifest.entries.map((entry) => {
259
+ const svg = thumbnails.get(entry.id);
260
+ return svg === undefined ? entry : { ...entry, thumbnailSvg: svg };
261
+ }),
262
+ };
225
263
  };