@askewly/design 0.4.0 → 0.4.1
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/dist/kickstart.js +67 -15
- package/dist/verify.js +16 -1
- package/package.json +1 -1
package/dist/kickstart.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { builtinModules } from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { createInterface } from "node:readline/promises";
|
|
4
5
|
import { stdin, stdout } from "node:process";
|
|
5
6
|
import { verifyDir } from "./verify.js";
|
|
7
|
+
const BUILTIN_MODULES = new Set(builtinModules);
|
|
6
8
|
export class KickstartError extends Error {
|
|
7
9
|
}
|
|
8
10
|
const REGISTRY_BASE = "https://ui.askewly.com";
|
|
@@ -309,6 +311,49 @@ function destFor(file, srcRoot) {
|
|
|
309
311
|
return path.join(srcRoot, "lib", libMatch[1]);
|
|
310
312
|
return path.join(srcRoot, "components", path.basename(p));
|
|
311
313
|
}
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// What the transplanted code actually needs installed.
|
|
316
|
+
//
|
|
317
|
+
// A registry item's declared `dependencies` is a lower bound, not the answer:
|
|
318
|
+
// shadcn's own button.json declares only `radix-ui` while button.tsx imports
|
|
319
|
+
// `class-variance-authority`. Following the printed `npm i` therefore left cva
|
|
320
|
+
// missing and the first fresh build failed (M27). The authority is the source
|
|
321
|
+
// we just wrote to disk, so read the imports out of it.
|
|
322
|
+
// ---------------------------------------------------------------------------
|
|
323
|
+
const SPECIFIER = /(?:\bfrom\s*|\brequire\s*\(\s*|\bimport\s*\(\s*|^\s*import\s+)(['"])([^'"]+)\1/gm;
|
|
324
|
+
/** Bare npm package names imported by a source file, folded to their install name. */
|
|
325
|
+
export function importedPackages(content) {
|
|
326
|
+
const found = new Set();
|
|
327
|
+
for (const match of content.matchAll(SPECIFIER)) {
|
|
328
|
+
const spec = match[2];
|
|
329
|
+
// Relative paths and the project's own `@/` alias are not packages.
|
|
330
|
+
if (spec.startsWith(".") || spec.startsWith("/") || spec.startsWith("@/"))
|
|
331
|
+
continue;
|
|
332
|
+
if (spec.startsWith("node:"))
|
|
333
|
+
continue;
|
|
334
|
+
if (BUILTIN_MODULES.has(spec.split("/")[0]))
|
|
335
|
+
continue;
|
|
336
|
+
const parts = spec.split("/");
|
|
337
|
+
// `radix-ui/react-slot` installs as `radix-ui`; `@scope/pkg/sub` as `@scope/pkg`.
|
|
338
|
+
found.add(spec.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0]);
|
|
339
|
+
}
|
|
340
|
+
return [...found];
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* The symbol a consumer imports to render the block.
|
|
344
|
+
*
|
|
345
|
+
* Derived from the transplanted page rather than declared in the registry:
|
|
346
|
+
* a meta field would mean regenerating and redeploying all 56 assets for a
|
|
347
|
+
* defect the CLI can close on its own. `…Demo` exports are the block's own
|
|
348
|
+
* showcase wrapper, never the entry point.
|
|
349
|
+
*/
|
|
350
|
+
export function blockExportName(pageSource) {
|
|
351
|
+
for (const match of pageSource.matchAll(/^\s*export\s+function\s+([A-Za-z_$][\w$]*)/gm)) {
|
|
352
|
+
if (!match[1].endsWith("Demo"))
|
|
353
|
+
return match[1];
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
312
357
|
export async function fetchBlock(blockName, registryBase) {
|
|
313
358
|
const block = await fetchJson(`${registryBase}/r/${blockName}.json`);
|
|
314
359
|
if (block.meta?.tier !== "block") {
|
|
@@ -321,6 +366,16 @@ export async function transplantBlock(block, targetDir) {
|
|
|
321
366
|
const written = [];
|
|
322
367
|
const npmDeps = new Set(["clsx", "tailwind-merge"]);
|
|
323
368
|
const visited = new Set();
|
|
369
|
+
function write(file) {
|
|
370
|
+
const dest = destFor(file, srcRoot);
|
|
371
|
+
const content = rewriteImports(file.content);
|
|
372
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
373
|
+
writeFileSync(dest, content);
|
|
374
|
+
written.push(dest);
|
|
375
|
+
// The file we just wrote is the authority on what must be installed.
|
|
376
|
+
for (const pkg of importedPackages(content))
|
|
377
|
+
npmDeps.add(pkg);
|
|
378
|
+
}
|
|
324
379
|
async function resolve(item) {
|
|
325
380
|
for (const dep of item.dependencies ?? [])
|
|
326
381
|
npmDeps.add(dep.split("@").slice(0, dep.startsWith("@") ? 2 : 1).join("@"));
|
|
@@ -332,21 +387,13 @@ export async function transplantBlock(block, targetDir) {
|
|
|
332
387
|
visited.add(key);
|
|
333
388
|
const child = await fetchJson(url);
|
|
334
389
|
await resolve(child);
|
|
335
|
-
for (const file of child.files ?? [])
|
|
336
|
-
|
|
337
|
-
mkdirSync(path.dirname(dest), { recursive: true });
|
|
338
|
-
writeFileSync(dest, rewriteImports(file.content));
|
|
339
|
-
written.push(dest);
|
|
340
|
-
}
|
|
390
|
+
for (const file of child.files ?? [])
|
|
391
|
+
write(file);
|
|
341
392
|
}
|
|
342
393
|
}
|
|
343
394
|
await resolve(block);
|
|
344
|
-
for (const file of block.files ?? [])
|
|
345
|
-
|
|
346
|
-
mkdirSync(path.dirname(dest), { recursive: true });
|
|
347
|
-
writeFileSync(dest, rewriteImports(file.content));
|
|
348
|
-
written.push(dest);
|
|
349
|
-
}
|
|
395
|
+
for (const file of block.files ?? [])
|
|
396
|
+
write(file);
|
|
350
397
|
// cn() — shadcn init would have created it; transplant guarantees it (M18 step-5 friction).
|
|
351
398
|
const utils = path.join(srcRoot, "lib", "utils.ts");
|
|
352
399
|
if (!existsSync(utils)) {
|
|
@@ -401,10 +448,15 @@ export async function runKickstart(dir, opts) {
|
|
|
401
448
|
throw new KickstartError("transplanted block bypasses the token system — fix before continuing");
|
|
402
449
|
}
|
|
403
450
|
console.log(`verify PASS — ${files} block file(s), no color literals`);
|
|
404
|
-
// 7. handoff
|
|
451
|
+
// 7. handoff — every line here must be true of the project that now exists on
|
|
452
|
+
// disk. A printed step the reader cannot follow is worse than no step.
|
|
453
|
+
const pagePath = path.join(blockDir, "page.tsx");
|
|
454
|
+
const exportName = existsSync(pagePath) ? blockExportName(readFileSync(pagePath, "utf8")) : null;
|
|
405
455
|
console.log(`\nNext steps:`);
|
|
406
456
|
console.log(` 1. npm i ${[...result.npmDeps].sort().join(" ")}`);
|
|
407
457
|
console.log(` 2. import "./askewly-brand.css" after \`@import "tailwindcss";\` in your global stylesheet`);
|
|
408
|
-
console.log(
|
|
458
|
+
console.log(exportName
|
|
459
|
+
? ` 3. render the block: import { ${exportName} } from "@/components/blocks/${opts.block}/page"`
|
|
460
|
+
: ` 3. render the block from "@/components/blocks/${opts.block}/page" — open it to see which component it exports`);
|
|
409
461
|
console.log(` 4. wire routing yourself — the block deliberately ships none (block-contract §3)`);
|
|
410
462
|
}
|
package/dist/verify.js
CHANGED
|
@@ -30,6 +30,21 @@ const LINE_COMMENT_EXTENSIONS = new Set(["ts", "tsx", "js", "jsx"]);
|
|
|
30
30
|
// Two SVG shapes: paired <svg>…</svg> and self-closing <svg … />.
|
|
31
31
|
const SVG_PAIRED = /<svg\b[\s\S]*?<\/svg\s*>/gi;
|
|
32
32
|
const SVG_SELF_CLOSING = /<svg\b[^>]*\/>/gi;
|
|
33
|
+
/**
|
|
34
|
+
* CSS attribute selectors whose value is a hex color, e.g. the Tailwind
|
|
35
|
+
* arbitrary variant `[&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border`.
|
|
36
|
+
*
|
|
37
|
+
* The hex there is a *match target*, not a color the file paints with: it names
|
|
38
|
+
* a value some third-party library already emitted so the rule can override it
|
|
39
|
+
* with a token. Flagging it inverts the gate — the line doing the token work is
|
|
40
|
+
* the line getting reported.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately narrow: an attribute name, an `=` (optionally prefixed by a CSS
|
|
43
|
+
* match operator), and a *quoted* hex. Tailwind arbitrary values like
|
|
44
|
+
* `bg-[#ccc]` have no `=` and no quotes, so they stay violations. The optional
|
|
45
|
+
* backslashes cover selectors written inside a same-quoted JS string.
|
|
46
|
+
*/
|
|
47
|
+
const ATTR_SELECTOR_HEX = /\[[A-Za-z_-][\w:.-]*\s*[~^|*$]?=\s*\\?(['"])#[0-9a-fA-F]{3,8}\\?\1\s*[iIsS]?\s*\]/g;
|
|
33
48
|
/**
|
|
34
49
|
* Blanks regions whose color literals are not rendered output — comments and
|
|
35
50
|
* SVG markup — while preserving every character position and newline, so
|
|
@@ -109,7 +124,7 @@ export function maskIgnoredRegions(source, ext) {
|
|
|
109
124
|
// Pass 2 — SVG markup, matched against the comment-masked text so a commented
|
|
110
125
|
// out `</svg>` cannot terminate a block early.
|
|
111
126
|
let masked = chars.join("");
|
|
112
|
-
for (const pattern of [SVG_PAIRED, SVG_SELF_CLOSING]) {
|
|
127
|
+
for (const pattern of [SVG_PAIRED, SVG_SELF_CLOSING, ATTR_SELECTOR_HEX]) {
|
|
113
128
|
masked = masked.replace(pattern, (match) => match.replace(/[^\n]/g, " "));
|
|
114
129
|
}
|
|
115
130
|
return masked;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@askewly/design",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Query, inject, and verify the Askewly Design system (tokens, UI vocabulary, recipes) from your terminal or coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://ui.askewly.com",
|