lineadraw 0.2.0 → 0.4.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/README.md +133 -0
- package/bin/lineadraw.mjs +98 -0
- package/lib/init.mjs +127 -0
- package/lib/manifest.mjs +225 -0
- package/package.json +34 -18
- package/templates/README.md +18 -0
- package/templates/sample.block.ts +35 -0
- package/templates/tsconfig.json +11 -0
- package/templates/workflow.yml +18 -0
- package/main.js +0 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# lineadraw
|
|
2
|
+
|
|
3
|
+
Type definitions and marketplace tooling for [Linea](https://lineadraw.com)
|
|
4
|
+
blocks and commands — the one devDependency a block/command repository
|
|
5
|
+
needs. Zero runtime: your modules import types from `"lineadraw"`, and the
|
|
6
|
+
Linea editor provides the real implementation when it runs them.
|
|
7
|
+
|
|
8
|
+
- **Typings** — `lineadraw.d.ts` makes `import { defineBlock } from
|
|
9
|
+
"lineadraw"` and `import { … } from "lineadraw/helpers"` type-check in
|
|
10
|
+
any editor/CI, with parameter types inferred into your `draw` function.
|
|
11
|
+
- **CLI** — `lineadraw manifest | check | init` turns any repository into
|
|
12
|
+
a Linea **marketplace**: a git repo the editor can browse and install
|
|
13
|
+
blocks/commands from.
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
Scaffold a fresh marketplace repository:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm create lineadraw my-blocks
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or add the structure to an existing project:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm i -D lineadraw
|
|
27
|
+
npx lineadraw init
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Either way you get `blocks/` and `commands/` folders, a `tsconfig.json`
|
|
31
|
+
wired to the typings, a sample block, a CI workflow, and a generated
|
|
32
|
+
catalog (`.lineadraw/marketplace.json`) — the repo is addable in the
|
|
33
|
+
editor from the first push.
|
|
34
|
+
|
|
35
|
+
## Writing a block
|
|
36
|
+
|
|
37
|
+
A block is one module with a single `defineBlock` default export:
|
|
38
|
+
typed parameters in, geometry out.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
// blocks/surface.block.ts
|
|
42
|
+
import { defineBlock } from "lineadraw";
|
|
43
|
+
|
|
44
|
+
export default defineBlock({
|
|
45
|
+
// The id is the block's identity everywhere: @<collection>/<slug>.
|
|
46
|
+
// Re-publishing the same id updates installed copies in place.
|
|
47
|
+
id: "@my-blocks/surface",
|
|
48
|
+
name: "Surface",
|
|
49
|
+
version: "1.0.0",
|
|
50
|
+
params: [
|
|
51
|
+
{ name: "kind", type: "enum", default: "rock", options: ["rock", "ground"] },
|
|
52
|
+
],
|
|
53
|
+
place: ["Start point", "End point"], // one point pick per label, in order
|
|
54
|
+
draw: ({ params, inputs: [start, end] }) => {
|
|
55
|
+
params.kind; // "rock" | "ground" — inferred from the params table
|
|
56
|
+
return [{ type: "line", a: start, b: end }];
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Notable optional members: `place` may instead be an interactive
|
|
62
|
+
`({ params, pickPoint, pickObject })` function run once at insertion;
|
|
63
|
+
`dependencies` lists ids of blocks your `draw` emits instances of (installs
|
|
64
|
+
pull them along); `paramVisibility({ params })` hides property rows;
|
|
65
|
+
`previewInputs` curates the inputs used for thumbnails. Units are
|
|
66
|
+
millimeters, Y-up, angles in radians CCW.
|
|
67
|
+
|
|
68
|
+
## Writing a command
|
|
69
|
+
|
|
70
|
+
A command is a macro with a single `defineCommand` default export. Its
|
|
71
|
+
whole run is one undo step; Esc during a pick reverts cleanly.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// commands/circle-here.cmd.ts
|
|
75
|
+
import { defineCommand } from "lineadraw";
|
|
76
|
+
|
|
77
|
+
export default defineCommand({
|
|
78
|
+
id: "@my-blocks/circle-here",
|
|
79
|
+
name: "Circle here",
|
|
80
|
+
run: async ({ document, pickPoint, prompt, showToast }) => {
|
|
81
|
+
const center = await pickPoint("Center point");
|
|
82
|
+
const r = await prompt("Radius");
|
|
83
|
+
document.add([{ type: "circle", center, radius: Number.parseFloat(r) }]);
|
|
84
|
+
showToast("Done", "success");
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Imports are limited to `"lineadraw"` and `"lineadraw/helpers"` (geometry
|
|
90
|
+
helpers) — the editor resolves both when executing your module.
|
|
91
|
+
|
|
92
|
+
## The CLI
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
lineadraw manifest Validate all modules and write .lineadraw/marketplace.json
|
|
96
|
+
lineadraw check Validate without writing; fail if the committed manifest is stale
|
|
97
|
+
lineadraw init Add the marketplace structure to the current npm project
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The manifest is deterministic (repo-relative paths, no git metadata), so
|
|
101
|
+
`check` is a byte-for-byte diff — the included CI workflow runs it on every
|
|
102
|
+
push and PR. With `@lineadraw/sdk` installed as a devDependency,
|
|
103
|
+
`manifest`/`check` additionally **dry-run every block** and render
|
|
104
|
+
thumbnails; without it the CLI still fully works.
|
|
105
|
+
|
|
106
|
+
Collection name and owner come from your package.json:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "lineadraw": { "marketplaceName": "My blocks", "marketplaceOwner": "Me" } }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Using a marketplace in the editor
|
|
113
|
+
|
|
114
|
+
In [the editor](https://lineadraw.com): **Library → Marketplace tab → +**
|
|
115
|
+
and enter `owner/repo` (or `owner/repo@branch`). For a private repository,
|
|
116
|
+
create a fine-grained personal access token with read-only Contents access
|
|
117
|
+
to just that repository and paste it in the add dialog — it is stored only
|
|
118
|
+
on that device. Installing embeds the module (and its declared dependency
|
|
119
|
+
closure) in the document, so drawings keep working offline and travel with
|
|
120
|
+
the file.
|
|
121
|
+
|
|
122
|
+
## Ecosystem
|
|
123
|
+
|
|
124
|
+
- [Linea editor](https://lineadraw.com) — the browser CAD app that runs
|
|
125
|
+
these modules.
|
|
126
|
+
- [`lineadraw/lineadraw`](https://github.com/lineadraw/lineadraw) — the
|
|
127
|
+
public collection: real blocks/commands to learn from, agent skill, and
|
|
128
|
+
guides.
|
|
129
|
+
- [`@lineadraw/sdk`](https://www.npmjs.com/package/@lineadraw/sdk) —
|
|
130
|
+
headless Node API over the same kernel (open/edit/render/export
|
|
131
|
+
`.linea` documents; also powers the CLI's block dry-runs).
|
|
132
|
+
- [`@lineadraw/mcp`](https://www.npmjs.com/package/@lineadraw/mcp) — MCP
|
|
133
|
+
server with the full editor as an in-chat app.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `lineadraw` CLI — marketplace tooling for Linea block/command repos.
|
|
3
|
+
// lineadraw manifest [--dir <path>] generate .lineadraw/marketplace.json
|
|
4
|
+
// lineadraw check [--dir <path>] validate + fail if the manifest is stale
|
|
5
|
+
// lineadraw init [--dir <path>] add the marketplace structure here
|
|
6
|
+
// See docs/marketplace-devx.md in the linea repo for the design.
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import {
|
|
10
|
+
buildManifest,
|
|
11
|
+
manifestJson,
|
|
12
|
+
MANIFEST_PATH,
|
|
13
|
+
verifyWithSdk,
|
|
14
|
+
} from "../lib/manifest.mjs";
|
|
15
|
+
import { init } from "../lib/init.mjs";
|
|
16
|
+
|
|
17
|
+
const USAGE = `Usage: lineadraw <manifest|check|init> [--dir <path>]
|
|
18
|
+
|
|
19
|
+
manifest Validate all modules and write ${MANIFEST_PATH}
|
|
20
|
+
check Validate without writing; fail if the committed manifest is stale
|
|
21
|
+
init Add the marketplace structure to the current npm project
|
|
22
|
+
|
|
23
|
+
With @lineadraw/sdk installed (devDependency), manifest and check also
|
|
24
|
+
dry-run every block. The manifest content is identical either way.`;
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2);
|
|
27
|
+
const command = args[0];
|
|
28
|
+
const dirFlag = args.indexOf("--dir");
|
|
29
|
+
const dir = resolve(dirFlag === -1 ? "." : (args[dirFlag + 1] ?? "."));
|
|
30
|
+
|
|
31
|
+
const fail = (message) => {
|
|
32
|
+
console.error(message);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const reportProblems = (problems) => {
|
|
37
|
+
for (const p of problems)
|
|
38
|
+
console.error(`REJECTED ${p.file}: ${p.problems.join("; ")}`);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Validate everything (light + optional sdk dry-run); exits on failure. */
|
|
42
|
+
const validatedManifest = async () => {
|
|
43
|
+
const { manifest, sources, problems } = buildManifest(dir);
|
|
44
|
+
const sdkResult = await verifyWithSdk(dir, manifest, sources);
|
|
45
|
+
const all = [...problems, ...sdkResult.problems];
|
|
46
|
+
if (all.length > 0) {
|
|
47
|
+
reportProblems(all);
|
|
48
|
+
fail(`${all.length} problem(s) — nothing written.`);
|
|
49
|
+
}
|
|
50
|
+
if (!sdkResult.ran)
|
|
51
|
+
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.",
|
|
53
|
+
);
|
|
54
|
+
return manifest;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
switch (command) {
|
|
58
|
+
case "manifest": {
|
|
59
|
+
const manifest = await validatedManifest();
|
|
60
|
+
const outPath = join(dir, MANIFEST_PATH);
|
|
61
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
62
|
+
writeFileSync(outPath, manifestJson(manifest));
|
|
63
|
+
console.log(`${MANIFEST_PATH}: ${manifest.entries.length} entries`);
|
|
64
|
+
for (const e of manifest.entries)
|
|
65
|
+
console.log(` ${e.kind} ${e.id} v${e.version}`);
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
case "check": {
|
|
69
|
+
const manifest = await validatedManifest();
|
|
70
|
+
const committedPath = join(dir, MANIFEST_PATH);
|
|
71
|
+
const committed = existsSync(committedPath)
|
|
72
|
+
? readFileSync(committedPath, "utf8")
|
|
73
|
+
: null;
|
|
74
|
+
if (committed !== manifestJson(manifest))
|
|
75
|
+
fail(
|
|
76
|
+
`${MANIFEST_PATH} is ${committed === null ? "missing" : "stale"} — run \`npx lineadraw manifest\` and commit the result.`,
|
|
77
|
+
);
|
|
78
|
+
console.log(
|
|
79
|
+
`OK: ${manifest.entries.length} entries, manifest is current.`,
|
|
80
|
+
);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
case "init": {
|
|
84
|
+
try {
|
|
85
|
+
const { created, skipped } = init(dir);
|
|
86
|
+
for (const f of created) console.log(`created ${f}`);
|
|
87
|
+
for (const f of skipped) console.log(`kept ${f}`);
|
|
88
|
+
console.log(
|
|
89
|
+
"\nDone. Commit, push, then add the repo in the editor: Library → Marketplace.",
|
|
90
|
+
);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
fail(e instanceof Error ? e.message : String(e));
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
default:
|
|
97
|
+
fail(USAGE);
|
|
98
|
+
}
|
package/lib/init.mjs
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// `lineadraw init` — add the marketplace structure to the CURRENT npm
|
|
2
|
+
// project, whatever it is. Non-destructive: existing files and package.json
|
|
3
|
+
// keys are never overwritten; everything it creates is reported. Ends by
|
|
4
|
+
// generating `.lineadraw/marketplace.json`, so the repo is addable in the
|
|
5
|
+
// editor from the first push.
|
|
6
|
+
import {
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readFileSync,
|
|
10
|
+
readdirSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { buildManifest, manifestJson, MANIFEST_PATH } from "./manifest.mjs";
|
|
16
|
+
|
|
17
|
+
const templatesDir = join(
|
|
18
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
19
|
+
"..",
|
|
20
|
+
"templates",
|
|
21
|
+
);
|
|
22
|
+
const template = (name) => readFileSync(join(templatesDir, name), "utf8");
|
|
23
|
+
|
|
24
|
+
/** Lowercase-kebab collection slug from a package name (`@scope/` and
|
|
25
|
+
* illegal characters dropped, per the `@collection/slug` id grammar). */
|
|
26
|
+
export const collectionSlug = (pkgName) => {
|
|
27
|
+
const slug = String(pkgName ?? "")
|
|
28
|
+
.replace(/^@[^/]+\//, "")
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
31
|
+
.replace(/^-+|-+$/g, "")
|
|
32
|
+
.slice(0, 64);
|
|
33
|
+
return slug === "" ? "my-collection" : slug;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Run init in `dir`. Returns `{ created, skipped }` path lists. */
|
|
37
|
+
export const init = (dir) => {
|
|
38
|
+
const pkgPath = join(dir, "package.json");
|
|
39
|
+
if (!existsSync(pkgPath))
|
|
40
|
+
throw new Error(
|
|
41
|
+
"no package.json here — run `npm init` first (or start from an empty folder with `npm create lineadraw`)",
|
|
42
|
+
);
|
|
43
|
+
const created = [];
|
|
44
|
+
const skipped = [];
|
|
45
|
+
const write = (relPath, content) => {
|
|
46
|
+
const abs = join(dir, relPath);
|
|
47
|
+
if (existsSync(abs)) {
|
|
48
|
+
skipped.push(relPath);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
52
|
+
writeFileSync(abs, content);
|
|
53
|
+
created.push(relPath);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// package.json: fill the marketplace config and scripts, never replacing
|
|
57
|
+
// what is already there.
|
|
58
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
59
|
+
let pkgChanged = false;
|
|
60
|
+
if (pkg.lineadraw === undefined) {
|
|
61
|
+
pkg.lineadraw = {
|
|
62
|
+
marketplaceName: pkg.description ?? pkg.name ?? "Marketplace",
|
|
63
|
+
marketplaceOwner:
|
|
64
|
+
typeof pkg.author === "string" ? pkg.author : (pkg.author?.name ?? ""),
|
|
65
|
+
};
|
|
66
|
+
pkgChanged = true;
|
|
67
|
+
}
|
|
68
|
+
pkg.scripts ??= {};
|
|
69
|
+
for (const [script, cmd] of [
|
|
70
|
+
["manifest", "lineadraw manifest"],
|
|
71
|
+
["check", "lineadraw check"],
|
|
72
|
+
]) {
|
|
73
|
+
if (pkg.scripts[script] === undefined) {
|
|
74
|
+
pkg.scripts[script] = cmd;
|
|
75
|
+
pkgChanged = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (pkgChanged) {
|
|
79
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
80
|
+
created.push("package.json (lineadraw config + scripts merged)");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const blocksDir = pkg.lineadraw?.blocks ?? "blocks";
|
|
84
|
+
const commandsDir = pkg.lineadraw?.commands ?? "commands";
|
|
85
|
+
mkdirSync(join(dir, blocksDir), { recursive: true });
|
|
86
|
+
mkdirSync(join(dir, commandsDir), { recursive: true });
|
|
87
|
+
|
|
88
|
+
// A sample block only when the blocks folder is empty — init on an
|
|
89
|
+
// existing collection must not inject content.
|
|
90
|
+
const hasModules = readdirSync(join(dir, blocksDir)).some(
|
|
91
|
+
(f) => f.endsWith(".ts") || f.endsWith(".js"),
|
|
92
|
+
);
|
|
93
|
+
if (!hasModules) {
|
|
94
|
+
const author = pkg.lineadraw?.marketplaceOwner || "Your Name";
|
|
95
|
+
write(
|
|
96
|
+
join(blocksDir, "sample-box.block.ts"),
|
|
97
|
+
template("sample.block.ts")
|
|
98
|
+
.replaceAll("__COLLECTION__", collectionSlug(pkg.name))
|
|
99
|
+
.replaceAll("__AUTHOR__", author),
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
write(".github/workflows/marketplace.yml", template("workflow.yml"));
|
|
104
|
+
write("tsconfig.json", template("tsconfig.json"));
|
|
105
|
+
write(
|
|
106
|
+
"README.md",
|
|
107
|
+
template("README.md").replaceAll(
|
|
108
|
+
"__NAME__",
|
|
109
|
+
pkg.lineadraw?.marketplaceName ?? pkg.name ?? "Marketplace",
|
|
110
|
+
),
|
|
111
|
+
);
|
|
112
|
+
write(".gitignore", "node_modules/\n");
|
|
113
|
+
|
|
114
|
+
// Generate the catalog last, over whatever modules now exist.
|
|
115
|
+
const { manifest, problems } = buildManifest(dir);
|
|
116
|
+
if (problems.length > 0) {
|
|
117
|
+
const detail = problems
|
|
118
|
+
.map((p) => `${p.file}: ${p.problems.join("; ")}`)
|
|
119
|
+
.join("\n");
|
|
120
|
+
throw new Error(`existing modules failed validation:\n${detail}`);
|
|
121
|
+
}
|
|
122
|
+
mkdirSync(join(dir, dirname(MANIFEST_PATH)), { recursive: true });
|
|
123
|
+
writeFileSync(join(dir, MANIFEST_PATH), manifestJson(manifest));
|
|
124
|
+
created.push(`${MANIFEST_PATH} (${manifest.entries.length} entries)`);
|
|
125
|
+
|
|
126
|
+
return { created, skipped };
|
|
127
|
+
};
|
package/lib/manifest.mjs
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Minimal-manifest generator (docs/marketplace-devx.md in the linea repo).
|
|
2
|
+
// Pure filesystem walk — no git: rows carry repo-RELATIVE `source` paths
|
|
3
|
+
// that the editor resolves against the repo the user added, at the ref the
|
|
4
|
+
// manifest itself was fetched from. Deterministic: same content, same
|
|
5
|
+
// bytes, so `check` is a plain diff.
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { pathToFileURL } from "node:url";
|
|
10
|
+
|
|
11
|
+
// sucrase is this package's own dependency — the light path needs nothing
|
|
12
|
+
// else installed.
|
|
13
|
+
const require_ = createRequire(import.meta.url);
|
|
14
|
+
|
|
15
|
+
const ID_RE = /^@[a-z0-9-]{1,64}\/[a-z0-9-]{1,64}$/;
|
|
16
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
17
|
+
|
|
18
|
+
/** Marketplace config resolved from the repo's package.json. */
|
|
19
|
+
export const readConfig = (repoDir) => {
|
|
20
|
+
const pkgPath = join(repoDir, "package.json");
|
|
21
|
+
if (!existsSync(pkgPath))
|
|
22
|
+
throw new Error(`no package.json in ${repoDir} — run \`lineadraw init\` in an npm project`);
|
|
23
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
24
|
+
const cfg = pkg.lineadraw ?? {};
|
|
25
|
+
return {
|
|
26
|
+
name: cfg.marketplaceName ?? pkg.description ?? pkg.name ?? "Marketplace",
|
|
27
|
+
owner:
|
|
28
|
+
cfg.marketplaceOwner ??
|
|
29
|
+
(typeof pkg.author === "string" ? pkg.author : (pkg.author?.name ?? "")),
|
|
30
|
+
blocksDir: cfg.blocks ?? "blocks",
|
|
31
|
+
commandsDir: cfg.commands ?? "commands",
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Same execution model as @lineadraw/core's transpiler: ESM→CJS, run
|
|
36
|
+
// against a require shim. Helpers resolve to no-op function proxies —
|
|
37
|
+
// top-level code only needs the imports to EXIST; draw()/run() never run
|
|
38
|
+
// here (that is the optional sdk dry-run's job).
|
|
39
|
+
const helperProxy = new Proxy({}, { get: () => () => ({}) });
|
|
40
|
+
|
|
41
|
+
const executeModule = (source) => {
|
|
42
|
+
const { transform } = require_("sucrase");
|
|
43
|
+
const code = transform(source, {
|
|
44
|
+
transforms: ["typescript", "imports"],
|
|
45
|
+
}).code;
|
|
46
|
+
const moduleShim = { exports: {} };
|
|
47
|
+
new Function("exports", "require", "module", code)(
|
|
48
|
+
moduleShim.exports,
|
|
49
|
+
(spec) => {
|
|
50
|
+
if (spec === "lineadraw" || spec === "linea")
|
|
51
|
+
return {
|
|
52
|
+
helpers: helperProxy,
|
|
53
|
+
defineBlock: (s) => s,
|
|
54
|
+
defineCommand: (s) => s,
|
|
55
|
+
};
|
|
56
|
+
if (spec === "lineadraw/helpers" || spec === "linea/helpers")
|
|
57
|
+
return helperProxy;
|
|
58
|
+
throw new Error(`illegal import "${spec}"`);
|
|
59
|
+
},
|
|
60
|
+
moduleShim,
|
|
61
|
+
);
|
|
62
|
+
const exported = moduleShim.exports;
|
|
63
|
+
const spec = exported.default;
|
|
64
|
+
return spec !== null && typeof spec === "object" && !Array.isArray(spec)
|
|
65
|
+
? { ...exported, ...spec }
|
|
66
|
+
: exported;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const entryOf = (kind, repoDir, folder, file) => {
|
|
70
|
+
const source = readFileSync(join(repoDir, folder, file), "utf8");
|
|
71
|
+
let mod;
|
|
72
|
+
try {
|
|
73
|
+
mod = executeModule(source);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return { file, problems: [`does not compile: ${e.message}`] };
|
|
76
|
+
}
|
|
77
|
+
const problems = [];
|
|
78
|
+
if (typeof mod.id !== "string" || !ID_RE.test(mod.id))
|
|
79
|
+
problems.push(`id ${JSON.stringify(mod.id)} is not @collection/slug`);
|
|
80
|
+
if (typeof mod.name !== "string" || mod.name.trim() === "")
|
|
81
|
+
problems.push("missing name");
|
|
82
|
+
if (typeof mod.version !== "string" || !SEMVER_RE.test(mod.version))
|
|
83
|
+
problems.push(`version ${JSON.stringify(mod.version)} is not semver`);
|
|
84
|
+
if (
|
|
85
|
+
kind === "block" &&
|
|
86
|
+
typeof mod.draw !== "function" &&
|
|
87
|
+
typeof mod.main !== "function"
|
|
88
|
+
)
|
|
89
|
+
problems.push("no draw export");
|
|
90
|
+
if (kind === "command" && typeof mod.run !== "function")
|
|
91
|
+
problems.push("no run export");
|
|
92
|
+
if (problems.length > 0) return { file, problems };
|
|
93
|
+
return {
|
|
94
|
+
file,
|
|
95
|
+
source,
|
|
96
|
+
entry: {
|
|
97
|
+
kind,
|
|
98
|
+
id: mod.id,
|
|
99
|
+
name: mod.name,
|
|
100
|
+
version: mod.version,
|
|
101
|
+
...(typeof mod.description === "string"
|
|
102
|
+
? { description: mod.description }
|
|
103
|
+
: {}),
|
|
104
|
+
tags: Array.isArray(mod.tags)
|
|
105
|
+
? mod.tags.filter((t) => typeof t === "string")
|
|
106
|
+
: [],
|
|
107
|
+
authors: Array.isArray(mod.authors)
|
|
108
|
+
? mod.authors.filter((a) => typeof a === "string")
|
|
109
|
+
: [],
|
|
110
|
+
dependencies:
|
|
111
|
+
kind === "block" && Array.isArray(mod.dependencies)
|
|
112
|
+
? mod.dependencies.filter((d) => typeof d === "string")
|
|
113
|
+
: [],
|
|
114
|
+
// POSIX-relative, unencoded — the editor percent-encodes segments
|
|
115
|
+
// when it builds the fetch URL.
|
|
116
|
+
source: `${folder}/${file}`,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const listModules = (repoDir, folder) =>
|
|
122
|
+
existsSync(join(repoDir, folder))
|
|
123
|
+
? readdirSync(join(repoDir, folder))
|
|
124
|
+
.filter((f) => f.endsWith(".ts") || f.endsWith(".js"))
|
|
125
|
+
.sort()
|
|
126
|
+
: [];
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Build the manifest for a marketplace repo. Returns
|
|
130
|
+
* `{ manifest, sources, problems }`; `problems` is a list of
|
|
131
|
+
* `{ file, problems }` — a non-empty list means the repo must not publish
|
|
132
|
+
* (callers fail instead of writing a partial catalog). `sources` maps
|
|
133
|
+
* entry id → module source (for the optional sdk dry-run).
|
|
134
|
+
*/
|
|
135
|
+
export const buildManifest = (repoDir) => {
|
|
136
|
+
const cfg = readConfig(repoDir);
|
|
137
|
+
const results = [
|
|
138
|
+
...listModules(repoDir, cfg.blocksDir).map((f) =>
|
|
139
|
+
entryOf("block", repoDir, cfg.blocksDir, f),
|
|
140
|
+
),
|
|
141
|
+
...listModules(repoDir, cfg.commandsDir).map((f) =>
|
|
142
|
+
entryOf("command", repoDir, cfg.commandsDir, f),
|
|
143
|
+
),
|
|
144
|
+
];
|
|
145
|
+
const problems = results.filter((r) => r.problems).map((r) => ({
|
|
146
|
+
file: r.file,
|
|
147
|
+
problems: r.problems,
|
|
148
|
+
}));
|
|
149
|
+
const entries = results.filter((r) => r.entry).map((r) => r.entry);
|
|
150
|
+
|
|
151
|
+
const dupes = [
|
|
152
|
+
...new Set(
|
|
153
|
+
entries
|
|
154
|
+
.map((e) => e.id)
|
|
155
|
+
.filter((id, i, all) => all.indexOf(id) !== i),
|
|
156
|
+
),
|
|
157
|
+
];
|
|
158
|
+
if (dupes.length > 0)
|
|
159
|
+
problems.push({
|
|
160
|
+
file: "(catalog)",
|
|
161
|
+
problems: [`duplicate ids: ${dupes.join(", ")}`],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// The catalog must be dependency-CLOSED: every declared dependency
|
|
165
|
+
// resolves to a block entry in this same manifest.
|
|
166
|
+
const blockIds = new Set(
|
|
167
|
+
entries.filter((e) => e.kind === "block").map((e) => e.id),
|
|
168
|
+
);
|
|
169
|
+
const dangling = entries.flatMap((e) =>
|
|
170
|
+
e.dependencies
|
|
171
|
+
.filter((d) => !blockIds.has(d))
|
|
172
|
+
.map((d) => `${e.id} → ${d}`),
|
|
173
|
+
);
|
|
174
|
+
if (dangling.length > 0)
|
|
175
|
+
problems.push({
|
|
176
|
+
file: "(catalog)",
|
|
177
|
+
problems: [`dangling dependencies: ${dangling.join(", ")}`],
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const sources = new Map(
|
|
181
|
+
results.filter((r) => r.entry).map((r) => [r.entry.id, r.source]),
|
|
182
|
+
);
|
|
183
|
+
return {
|
|
184
|
+
manifest: {
|
|
185
|
+
name: cfg.name,
|
|
186
|
+
owner: { name: cfg.owner },
|
|
187
|
+
entries,
|
|
188
|
+
},
|
|
189
|
+
sources,
|
|
190
|
+
problems,
|
|
191
|
+
};
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export const manifestJson = (manifest) =>
|
|
195
|
+
JSON.stringify(manifest, null, 2) + "\n";
|
|
196
|
+
|
|
197
|
+
export const MANIFEST_PATH = ".lineadraw/marketplace.json";
|
|
198
|
+
|
|
199
|
+
/**
|
|
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.
|
|
204
|
+
*/
|
|
205
|
+
export const verifyWithSdk = async (repoDir, manifest, sources) => {
|
|
206
|
+
let sdk;
|
|
207
|
+
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: [] };
|
|
214
|
+
}
|
|
215
|
+
if (typeof sdk.testBlockDefinition !== "function")
|
|
216
|
+
return { ran: false, problems: [] }; // sdk predates module validation
|
|
217
|
+
const problems = [];
|
|
218
|
+
for (const entry of manifest.entries) {
|
|
219
|
+
if (entry.kind !== "block") continue; // commands are interactive — no dry run
|
|
220
|
+
const result = await sdk.testBlockDefinition(sources.get(entry.id));
|
|
221
|
+
if (!result.ok)
|
|
222
|
+
problems.push({ file: entry.source, problems: [result.error] });
|
|
223
|
+
}
|
|
224
|
+
return { ran: true, problems };
|
|
225
|
+
};
|
package/package.json
CHANGED
|
@@ -1,19 +1,35 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "lineadraw",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Type definitions for Linea
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"author": "Mikhail Koshelev",
|
|
7
|
-
"type": "module",
|
|
8
|
-
"types": "./lineadraw.d.ts",
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "lineadraw",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Type definitions and marketplace tooling for Linea blocks and commands",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Mikhail Koshelev",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"types": "./lineadraw.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"lineadraw": "./bin/lineadraw.mjs"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./lineadraw.d.ts"
|
|
15
|
+
},
|
|
16
|
+
"./helpers": {
|
|
17
|
+
"types": "./lineadraw.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"lineadraw.d.ts",
|
|
22
|
+
"bin",
|
|
23
|
+
"lib",
|
|
24
|
+
"templates"
|
|
25
|
+
],
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "vitest run"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"sucrase": "^3.35.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"vitest": "^4.1.10"
|
|
34
|
+
}
|
|
19
35
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# __NAME__
|
|
2
|
+
|
|
3
|
+
A [Linea](https://github.com/lineadraw/lineadraw) marketplace:
|
|
4
|
+
`blocks/*.block.ts` and `commands/*.cmd.ts` modules plus a generated
|
|
5
|
+
catalog (`.lineadraw/marketplace.json`).
|
|
6
|
+
|
|
7
|
+
## Authoring
|
|
8
|
+
|
|
9
|
+
- Add or edit modules under `blocks/` and `commands/`.
|
|
10
|
+
- Regenerate the catalog: `npm run manifest` — CI fails when it is stale.
|
|
11
|
+
- Validate without writing: `npm run check`.
|
|
12
|
+
|
|
13
|
+
## Using it in the editor
|
|
14
|
+
|
|
15
|
+
Library → Marketplace tab → + → add this repository
|
|
16
|
+
as `owner/repo`. For a private repository, create a fine-grained personal
|
|
17
|
+
access token with read-only Contents access to just this repository and
|
|
18
|
+
paste it in the add dialog — it is stored only on that device.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Sample block — a parametric rectangle centered on the picked point.
|
|
2
|
+
// Replace it with your own; the public collection at
|
|
3
|
+
// github.com/lineadraw/lineadraw is full of real examples.
|
|
4
|
+
import { defineBlock } from "lineadraw";
|
|
5
|
+
import type { Polyline } from "lineadraw";
|
|
6
|
+
|
|
7
|
+
export default defineBlock({
|
|
8
|
+
// The id is the block's identity everywhere: @<collection>/<slug>,
|
|
9
|
+
// lowercase kebab. Re-publishing the same id updates in place.
|
|
10
|
+
id: "@__COLLECTION__/sample-box",
|
|
11
|
+
name: "Sample box",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
description: "A parametric rectangle centered on the picked point.",
|
|
14
|
+
tags: ["sample"],
|
|
15
|
+
authors: ["__AUTHOR__"],
|
|
16
|
+
params: [
|
|
17
|
+
{ name: "width", label: "Width", type: "number", default: 100, min: 1 },
|
|
18
|
+
{ name: "height", label: "Height", type: "number", default: 50, min: 1 },
|
|
19
|
+
],
|
|
20
|
+
draw: ({ params }) => {
|
|
21
|
+
const w = params.width / 2;
|
|
22
|
+
const h = params.height / 2;
|
|
23
|
+
const box: Polyline = {
|
|
24
|
+
type: "polyline",
|
|
25
|
+
closed: true,
|
|
26
|
+
points: [
|
|
27
|
+
[-w, -h],
|
|
28
|
+
[w, -h],
|
|
29
|
+
[w, h],
|
|
30
|
+
[-w, h],
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
return [box];
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Validates every block/command module and checks the committed catalog
|
|
2
|
+
# (.lineadraw/marketplace.json) matches the sources in this commit.
|
|
3
|
+
# Stale catalog? Run `npx lineadraw manifest` and commit the result.
|
|
4
|
+
name: Marketplace
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
pull_request:
|
|
9
|
+
jobs:
|
|
10
|
+
check:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-node@v4
|
|
15
|
+
with:
|
|
16
|
+
node-version: 22
|
|
17
|
+
- run: npm ci
|
|
18
|
+
- run: npx lineadraw check
|
package/main.js
DELETED
|
File without changes
|