@pcbjs/cli 0.0.0 → 0.0.1-next.9029beb783788cfbb471cc9d57e1b47864c6eaa6

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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/dist/boards.d.ts +92 -0
  3. package/dist/boards.js +126 -0
  4. package/dist/bridge.d.ts +53 -0
  5. package/dist/bridge.js +147 -0
  6. package/dist/build.d.ts +21 -0
  7. package/dist/build.js +165 -0
  8. package/dist/check.d.ts +83 -0
  9. package/dist/check.js +261 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.js +278 -0
  12. package/dist/config.d.ts +70 -0
  13. package/dist/config.js +141 -0
  14. package/dist/create.d.ts +18 -0
  15. package/dist/create.js +73 -0
  16. package/dist/dev-reuse.d.ts +36 -0
  17. package/dist/dev-reuse.js +51 -0
  18. package/dist/docs.d.ts +21 -0
  19. package/dist/docs.js +101 -0
  20. package/dist/doctor.d.ts +31 -0
  21. package/dist/doctor.js +151 -0
  22. package/dist/drafts.d.ts +47 -0
  23. package/dist/drafts.js +113 -0
  24. package/dist/fetch-model-child.d.ts +1 -0
  25. package/dist/fetch-model-child.js +12 -0
  26. package/dist/fetch-model.d.ts +32 -0
  27. package/dist/fetch-model.js +177 -0
  28. package/dist/host-client.d.ts +60 -0
  29. package/dist/host-client.js +182 -0
  30. package/dist/ide.d.ts +38 -0
  31. package/dist/ide.js +119 -0
  32. package/dist/import.d.ts +9 -0
  33. package/dist/import.js +120 -0
  34. package/dist/index.d.ts +9 -0
  35. package/dist/index.js +8 -0
  36. package/dist/inspect-sch.d.ts +6 -0
  37. package/dist/inspect-sch.js +28 -0
  38. package/dist/inspect.d.ts +259 -0
  39. package/dist/inspect.js +451 -0
  40. package/dist/lsp.d.ts +25 -0
  41. package/dist/lsp.js +104 -0
  42. package/dist/model-child.d.ts +1 -0
  43. package/dist/model-child.js +26 -0
  44. package/dist/model.d.ts +101 -0
  45. package/dist/model.js +634 -0
  46. package/dist/models.d.ts +28 -0
  47. package/dist/models.js +236 -0
  48. package/dist/plugin.d.ts +59 -0
  49. package/dist/plugin.js +920 -0
  50. package/dist/prerender.d.ts +18 -0
  51. package/dist/prerender.js +79 -0
  52. package/dist/preview.d.ts +33 -0
  53. package/dist/preview.js +100 -0
  54. package/dist/project.d.ts +38 -0
  55. package/dist/project.js +154 -0
  56. package/dist/reference-child.d.ts +1 -0
  57. package/dist/reference-child.js +15 -0
  58. package/dist/references.d.ts +42 -0
  59. package/dist/references.js +328 -0
  60. package/dist/repo.d.ts +11 -0
  61. package/dist/repo.js +64 -0
  62. package/dist/scope-plugin.d.ts +2 -0
  63. package/dist/scope-plugin.js +22 -0
  64. package/dist/scope-state.d.ts +70 -0
  65. package/dist/scope-state.js +42 -0
  66. package/dist/scopes.d.ts +27 -0
  67. package/dist/scopes.js +96 -0
  68. package/dist/server.d.ts +55 -0
  69. package/dist/server.js +141 -0
  70. package/dist/shot.d.ts +74 -0
  71. package/dist/shot.js +300 -0
  72. package/dist/skills.d.ts +5 -0
  73. package/dist/skills.js +38 -0
  74. package/dist/source-ranges.d.ts +29 -0
  75. package/dist/source-ranges.js +261 -0
  76. package/dist/typecheck.d.ts +17 -0
  77. package/dist/typecheck.js +66 -0
  78. package/dist/version.d.ts +2 -0
  79. package/dist/version.js +12 -0
  80. package/dist/vite.d.ts +10 -0
  81. package/dist/vite.js +71 -0
  82. package/dist/workspace.d.ts +11 -0
  83. package/dist/workspace.js +53 -0
  84. package/docs/3d.md +100 -0
  85. package/docs/autorouter.md +108 -0
  86. package/docs/board.md +142 -0
  87. package/docs/circuits.md +140 -0
  88. package/docs/cli.md +158 -0
  89. package/docs/design.md +69 -0
  90. package/docs/development.md +94 -0
  91. package/docs/editor.md +112 -0
  92. package/docs/fab-set.md +87 -0
  93. package/docs/fab.md +99 -0
  94. package/docs/findings.md +86 -0
  95. package/docs/fonts.md +93 -0
  96. package/docs/highlight.md +45 -0
  97. package/docs/ide-bridge.md +145 -0
  98. package/docs/import.md +94 -0
  99. package/docs/inspect.md +25 -0
  100. package/docs/kicad.md +93 -0
  101. package/docs/layout.md +137 -0
  102. package/docs/length.md +37 -0
  103. package/docs/models.md +40 -0
  104. package/docs/pairs.md +126 -0
  105. package/docs/pours.md +74 -0
  106. package/docs/quickstart.md +126 -0
  107. package/docs/references.md +88 -0
  108. package/docs/routing.md +146 -0
  109. package/docs/rules.md +118 -0
  110. package/docs/schematic-graphic.md +137 -0
  111. package/docs/schematic.md +172 -0
  112. package/docs/skills.md +42 -0
  113. package/docs/sourcing.md +68 -0
  114. package/docs/stackup-and-rules.md +144 -0
  115. package/docs/stdlib.md +176 -0
  116. package/docs/viewer.md +107 -0
  117. package/docs/workspace.md +53 -0
  118. package/package.json +67 -3
  119. package/templates/board/README.md +14 -0
  120. package/templates/board/_gitignore +2 -0
  121. package/templates/board/package.json +16 -0
  122. package/templates/board/pcb.config.ts +8 -0
  123. package/templates/board/pcb.tsx +35 -0
  124. package/templates/board/tsconfig.json +15 -0
  125. package/vscode/manifest.json +9 -0
  126. package/vscode/vscode-pcbjs-0.0.1000.vsix +0 -0
  127. package/vscode/vscode-pcbjs.vsix +0 -0
package/dist/doctor.js ADDED
@@ -0,0 +1,151 @@
1
+ import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join, relative, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { ROUTER_VERSION } from '@pcbjs/compile/route-cache';
6
+ import { TUNE_VERSION } from '@pcbjs/compile/tune';
7
+ import { REFERENCE_VERSION } from './references.js';
8
+ import { findChrome } from './shot.js';
9
+ import { findConfig, resolveConfig } from './config.js';
10
+ import { installedModels } from '@pcbjs/kicad';
11
+ import { fetchedDir, KICAD_3D_TAG } from './fetch-model.js';
12
+ /** The editor extension, which ships inside @pcbjs/cli and is versioned on its own. */
13
+ const UNVERSIONED = new Set(['@pcbjs/vscode']);
14
+ const cliDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
15
+ export function cliVersion() {
16
+ try {
17
+ return JSON.parse(readFileSync(join(cliDir, 'package.json'), 'utf8')).version;
18
+ }
19
+ catch {
20
+ return '0.0.0';
21
+ }
22
+ }
23
+ /** The dist-tag a version is published under: beta for 0.0.1-beta.2, next for a snapshot, else latest. */
24
+ export function distTag(version) {
25
+ return /^\d+\.\d+\.\d+-([A-Za-z]+)/.exec(version)?.[1] ?? 'latest';
26
+ }
27
+ /** What a new project asks for: a caret on a prerelease also takes next snapshots, so the tag; else the minor a train shares. */
28
+ export function installRange(version) {
29
+ const tag = distTag(version);
30
+ return tag === 'latest' ? `~${version.split('.').slice(0, 2).join('.')}.0` : tag;
31
+ }
32
+ /** latest holds the 0.0.0 placeholder while the packages are in beta, so the fix names the running line. */
33
+ export const UPDATE_COMMAND = `npm install @pcbjs/pcbjs@${distTag(cliVersion())}`;
34
+ /** Every @pcbjs package a project resolves, nearest node_modules first, as Node resolves them. */
35
+ export function installedPackages(root) {
36
+ const found = new Map();
37
+ for (let dir = resolve(root);; dir = dirname(dir)) {
38
+ const scope = join(dir, 'node_modules', '@pcbjs');
39
+ if (existsSync(scope))
40
+ for (const sub of readdirSync(scope).sort()) {
41
+ const name = `@pcbjs/${sub}`;
42
+ const pkgFile = join(scope, sub, 'package.json');
43
+ if (found.has(name) || !existsSync(pkgFile))
44
+ continue;
45
+ try {
46
+ found.set(name, { name, version: JSON.parse(readFileSync(pkgFile, 'utf8')).version, dir: join(scope, sub) });
47
+ }
48
+ catch { }
49
+ }
50
+ if (dirname(dir) === dir)
51
+ break;
52
+ }
53
+ return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
54
+ }
55
+ /** major.minor, so 0.0.1 and 0.0.2 agree and 0.1.0-beta.1 and 0.1.3 agree. */
56
+ export function minorOf(version) {
57
+ const m = /^(\d+)\.(\d+)/.exec(version);
58
+ return m ? `${m[1]}.${m[2]}` : version;
59
+ }
60
+ /** Null when every package shares one major.minor, else the line naming each version and the fix. */
61
+ export function versionSkew(packages) {
62
+ packages = packages.filter(p => !UNVERSIONED.has(p.name));
63
+ const minors = new Set(packages.map(p => minorOf(p.version)));
64
+ if (minors.size < 2)
65
+ return null;
66
+ const list = packages.map(p => `${p.name} ${p.version}`).join(', ');
67
+ return `@pcbjs packages disagree on the version: ${list}; run ${UPDATE_COMMAND}`;
68
+ }
69
+ /** The installed packages plus the running CLI when it is another copy, such as a global install. */
70
+ export function packagesInUse(root) {
71
+ const installed = installedPackages(root);
72
+ const own = installed.find(p => p.name === '@pcbjs/cli');
73
+ if (own && realpathSync(own.dir) === realpathSync(cliDir))
74
+ return installed;
75
+ return [...installed, { name: 'pcbjs (running)', version: cliVersion(), dir: cliDir }];
76
+ }
77
+ const KEY = Symbol.for('pcbjs.cli.versionWarned');
78
+ /** The single warning line for a version skew, printed at most once per process. */
79
+ export function warnVersionSkew(root = process.cwd(), warn = l => console.warn(l)) {
80
+ const g = globalThis;
81
+ if (g[KEY])
82
+ return;
83
+ g[KEY] = true;
84
+ try {
85
+ const skew = versionSkew(packagesInUse(root));
86
+ if (skew)
87
+ warn(`pcbjs: warning: ${skew}`);
88
+ }
89
+ catch { }
90
+ }
91
+ export function stepConverter() {
92
+ try {
93
+ const step = createRequire(import.meta.url).resolve('@pcbjs/step/package.json');
94
+ const occt = createRequire(step).resolve('occt-import-js');
95
+ const wasm = join(dirname(occt), 'occt-import-js.wasm');
96
+ return existsSync(wasm) ? wasm : null;
97
+ }
98
+ catch {
99
+ return null;
100
+ }
101
+ }
102
+ /** Model files under a folder, its LICENSES.md left out. */
103
+ function countFiles(dir) {
104
+ try {
105
+ return readdirSync(dir, { recursive: true, withFileTypes: true }).filter(e => e.isFile() && e.name !== 'LICENSES.md' && !e.name.endsWith('.tmp')).length;
106
+ }
107
+ catch {
108
+ return 0;
109
+ }
110
+ }
111
+ export async function doctor(o = {}) {
112
+ const root = resolve(o.root ?? process.cwd());
113
+ const log = o.log ?? (l => console.log(l));
114
+ const show = (p) => (relative(root, p).startsWith('..') ? p : relative(root, p) || '.');
115
+ const row = (k, v) => log(`${k.padEnd(18)} ${v}`);
116
+ const packages = packagesInUse(root);
117
+ row('pcbjs', `${cliVersion()} ${show(cliDir)}`);
118
+ row('node', process.version);
119
+ if (!packages.length)
120
+ row('packages', `none under ${root}; run npm install`);
121
+ for (const p of packages)
122
+ row(p.name, `${p.version} ${show(p.dir)}`);
123
+ const chrome = findChrome();
124
+ row('chrome', chrome ?? 'not found; pcbjs shot needs one, set PCBJS_CHROME');
125
+ const wasm = stepConverter();
126
+ row('step converter', wasm ? show(wasm) : 'not found; STEP models will not load, reinstall @pcbjs/step');
127
+ row('formats', `router ${ROUTER_VERSION}, tune ${TUNE_VERSION}, reference ${REFERENCE_VERSION}`);
128
+ const found = o.config ? { file: resolve(root, o.config) } : findConfig(root);
129
+ if (!found)
130
+ row('config', 'none; pcbjs create <dir> starts a project');
131
+ else {
132
+ row('config', show(found.file));
133
+ try {
134
+ const cfg = await resolveConfig([], 'build', o.config, root, () => { });
135
+ row('entries', cfg.entries.map(e => e.rel).join(', '));
136
+ const lib = installedModels();
137
+ row('kicad models', lib && existsSync(lib) ? show(lib) : `none installed; kicad: models download from KiCad ${KICAD_3D_TAG} on first use`);
138
+ row('fetched models', `${countFiles(fetchedDir(cfg.root))} in ${show(fetchedDir(cfg.root))}; pcbjs model check counts the standard library's parts`);
139
+ }
140
+ catch (e) {
141
+ row('entries', `error: ${e.message.split('\n')[0]}`);
142
+ }
143
+ }
144
+ const skew = versionSkew(packages);
145
+ if (skew) {
146
+ log(`\nerror: ${skew}`);
147
+ return 1;
148
+ }
149
+ log('\nversions agree');
150
+ return 0;
151
+ }
@@ -0,0 +1,47 @@
1
+ import { type Plugin } from 'vite';
2
+ export interface Span {
3
+ start: number;
4
+ end: number;
5
+ }
6
+ export interface Draft {
7
+ /** the file as the editor names it, which may be through a symbolic link */
8
+ named: string;
9
+ /** the editor's text */
10
+ text: string;
11
+ /** what Vite compiles: the text, or its repair */
12
+ code: string;
13
+ /** the spans the repair blanked, offsets of `text` */
14
+ skipped: Span[];
15
+ /** where the repair's closers start: an offset at or past it maps to the one before */
16
+ limit: number | null;
17
+ /** neither the text nor a repair compiles: the last good board stays */
18
+ broken: boolean;
19
+ }
20
+ export interface Drafts {
21
+ /** the editor's text of a file, applied after a pause in typing; null drops the draft at once */
22
+ set(file: string, text: string | null, named?: string): void;
23
+ get(file: string): Draft | undefined;
24
+ files(): string[];
25
+ dispose(): void;
26
+ }
27
+ /** The drafts of a dev server; `changed` runs once a file's draft is applied or dropped. */
28
+ export declare function drafts(changed: (file: string) => void, delayMs?: number): Drafts;
29
+ /** The offset of the first syntax error esbuild finds, null when it takes the code. */
30
+ export declare function syntaxError(file: string, code: string): Promise<number | null>;
31
+ /** The draft of a text: as it is when esbuild takes it, else the repair, retried once with the offset esbuild still rejects. */
32
+ export declare function prepare(file: string, text: string): Promise<Omit<Draft, 'named'>>;
33
+ /** Serve each draft in place of its file. */
34
+ export declare function draftsPlugin(store: () => Drafts | null | undefined): Plugin;
35
+ interface Position {
36
+ line: number;
37
+ character: number;
38
+ }
39
+ /** The spans each draft's repair skipped, as LSP ranges of the editor's text. */
40
+ export declare function skippedRanges(store: Drafts | null | undefined): {
41
+ file: string;
42
+ ranges: {
43
+ start: Position;
44
+ end: Position;
45
+ }[];
46
+ }[];
47
+ export {};
package/dist/drafts.js ADDED
@@ -0,0 +1,113 @@
1
+ // docs/editor.md, Drafts, has the rules.
2
+ import { readFileSync } from 'node:fs';
3
+ import { transformWithEsbuild } from 'vite';
4
+ import { loadLsp } from './lsp.js';
5
+ /** The drafts of a dev server; `changed` runs once a file's draft is applied or dropped. */
6
+ export function drafts(changed, delayMs = 200) {
7
+ const map = new Map();
8
+ const timers = new Map();
9
+ const seq = new Map();
10
+ const apply = async (file, text, named, n) => {
11
+ let disk = null;
12
+ try {
13
+ disk = readFileSync(file, 'utf8');
14
+ }
15
+ catch { /* a file not saved yet */ }
16
+ const d = text === null || text === disk ? null : { ...(await prepare(file, text)), named };
17
+ if (seq.get(file) !== n)
18
+ return;
19
+ if (!d) {
20
+ if (map.delete(file))
21
+ changed(file);
22
+ return;
23
+ }
24
+ map.set(file, d);
25
+ changed(file);
26
+ };
27
+ return {
28
+ set(file, text, named = file) {
29
+ const n = (seq.get(file) ?? 0) + 1;
30
+ seq.set(file, n);
31
+ clearTimeout(timers.get(file));
32
+ timers.delete(file);
33
+ if (text === null) {
34
+ void apply(file, null, named, n);
35
+ return;
36
+ }
37
+ timers.set(file, setTimeout(() => { timers.delete(file); void apply(file, text, named, n); }, delayMs));
38
+ },
39
+ get: file => map.get(file),
40
+ files: () => [...map.keys()],
41
+ dispose() {
42
+ for (const t of timers.values())
43
+ clearTimeout(t);
44
+ timers.clear();
45
+ map.clear();
46
+ },
47
+ };
48
+ }
49
+ const LOADERS = { ts: 'ts', mts: 'ts', cts: 'ts', tsx: 'tsx', js: 'js', mjs: 'js', cjs: 'js', jsx: 'jsx' };
50
+ /** The offset of the first syntax error esbuild finds, null when it takes the code. */
51
+ export async function syntaxError(file, code) {
52
+ const loader = LOADERS[file.split('.').pop() ?? ''] ?? 'ts';
53
+ try {
54
+ await transformWithEsbuild(code, file, { loader, jsx: 'automatic', tsconfigRaw: {} });
55
+ return null;
56
+ }
57
+ catch (e) {
58
+ const at = e?.errors?.[0]?.location;
59
+ if (!at)
60
+ return 0;
61
+ const lines = code.split('\n');
62
+ const start = lines.slice(0, at.line - 1).reduce((n, l) => n + l.length + 1, 0);
63
+ // esbuild counts columns in UTF-8 bytes
64
+ return start + Buffer.from(lines[at.line - 1] ?? '').subarray(0, at.column).toString().length;
65
+ }
66
+ }
67
+ /** The draft of a text: as it is when esbuild takes it, else the repair, retried once with the offset esbuild still rejects. */
68
+ export async function prepare(file, text) {
69
+ const err = await syntaxError(file, text);
70
+ if (err === null)
71
+ return { text, code: text, skipped: [], limit: null, broken: false };
72
+ const { repair } = await loadLsp();
73
+ let r = repair(text, file);
74
+ let left = r.ok ? await syntaxError(file, r.text) : -1;
75
+ if (left !== null && left >= 0) {
76
+ r = repair(text, file, [Math.min(left, text.length)]);
77
+ left = r.ok ? await syntaxError(file, r.text) : -1;
78
+ }
79
+ if (left !== null)
80
+ return { text, code: text, skipped: [], limit: null, broken: true };
81
+ return { text, code: r.text, skipped: r.skipped, limit: r.limit, broken: false };
82
+ }
83
+ /** Serve each draft in place of its file. */
84
+ export function draftsPlugin(store) {
85
+ return {
86
+ name: 'pcbjs:drafts',
87
+ enforce: 'pre',
88
+ load(id) {
89
+ return store()?.get(id.split('?')[0])?.code;
90
+ },
91
+ };
92
+ }
93
+ /** The spans each draft's repair skipped, as LSP ranges of the editor's text. */
94
+ export function skippedRanges(store) {
95
+ const out = [];
96
+ for (const f of store?.files() ?? []) {
97
+ const d = store.get(f);
98
+ if (!d.skipped.length)
99
+ continue;
100
+ const starts = [0];
101
+ for (let i = 0; i < d.text.length; i++)
102
+ if (d.text.charCodeAt(i) === 10)
103
+ starts.push(i + 1);
104
+ const at = (o) => {
105
+ let line = 0;
106
+ while (line + 1 < starts.length && starts[line + 1] <= o)
107
+ line++;
108
+ return { line, character: o - starts[line] };
109
+ };
110
+ out.push({ file: d.named, ranges: d.skipped.map(s => ({ start: at(s.start), end: at(s.end) })) });
111
+ }
112
+ return out;
113
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ // node fetch-model-child.js <root> <source>: downloads one model for a compile that needs it at once; exit 1 with the reason, 2 when the network is down.
2
+ import { download } from './fetch-model.js';
3
+ const [root, source] = process.argv.slice(2);
4
+ try {
5
+ await download(root, source);
6
+ process.exit(0);
7
+ }
8
+ catch (e) {
9
+ const cause = e?.cause?.code ?? e?.cause?.message;
10
+ process.stderr.write(`${String(e?.message ?? e).split('\n')[0]}${cause ? `: ${cause}` : ''}\n`);
11
+ process.exit(e instanceof TypeError ? 2 : 1);
12
+ }
@@ -0,0 +1,32 @@
1
+ import { type RemoteModelResolver } from '@pcbjs/compile/models';
2
+ /** The tag of KiCad's 3D library the stdlib's models are aligned against. */
3
+ export declare const KICAD_3D_TAG = "10.0.7";
4
+ export declare const fetchedDir: (root: string) => string;
5
+ /** Where a fetched model is cached in a project. */
6
+ export declare function fetchedPath(root: string, source: string): string;
7
+ /** The download URL of a `kicad:` or https source. */
8
+ export declare const sourceUrl: (source: string) => string;
9
+ /** The licence line of a source, as far as its host says. */
10
+ export declare function licenceOf(source: string): string;
11
+ /** Append the source line of a model to <dir>/LICENSES.md, creating the file with its header. */
12
+ export declare function noteSource(dir: string, file: string, source: string, date?: string): string;
13
+ /** The STEP model of an LCSC part, no login: easyeda.com/api/products/<C-number>/components names it, modules.easyeda.com serves it. */
14
+ export declare function lcscModel(code: string, fetcher?: typeof fetch): Promise<{
15
+ data: Uint8Array;
16
+ name: string;
17
+ url: string;
18
+ note: string;
19
+ }>;
20
+ /** Download a source into the project's cache, the file written whole or not at all, and note its licence. */
21
+ export declare function download(root: string, source: string): Promise<string>;
22
+ /** Forget the failures of this process, for `model fetch`. */
23
+ export declare function retryFetches(): void;
24
+ /** The resolver the compile calls for each fetched model: the installed KiCad library, the cache, or a download in a child process. */
25
+ export declare function remoteResolver(root: string): RemoteModelResolver;
26
+ /** One line for the parts of a board whose models did not download, once per process for the same parts. */
27
+ export declare function fetchNotice(board: string, waiting: {
28
+ ref: string;
29
+ source: string;
30
+ }[]): string | null;
31
+ /** Let the next compile of this project resolve and fetch its models; the compile is synchronous, so each sets its own root. */
32
+ export declare function installModelFetch(root: string): void;
@@ -0,0 +1,177 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join, relative, sep } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { kicadModel } from '@pcbjs/kicad';
6
+ import { REMOTE_MODEL } from '@pcbjs/compile/models';
7
+ /** The tag of KiCad's 3D library the stdlib's models are aligned against. */
8
+ export const KICAD_3D_TAG = '10.0.7';
9
+ const KICAD_RAW = `https://gitlab.com/kicad/libraries/kicad-packages3D/-/raw/${KICAD_3D_TAG}/`;
10
+ const EASYEDA_MODELS = 'https://modules.easyeda.com/qAxj6KHrDKw4blvCG8QJPs7Y/';
11
+ const KICAD_LICENCE = 'CC-BY-SA 4.0 with the KiCad library exception, https://www.kicad.org/libraries/license/';
12
+ export const fetchedDir = (root) => join(root, '.pcbjs', 'models', 'fetched');
13
+ const kicadPath = (source) => source.slice('kicad:'.length).replace(/\.wrl$/i, '.step');
14
+ /** Where a fetched model is cached in a project. */
15
+ export function fetchedPath(root, source) {
16
+ const dir = fetchedDir(root);
17
+ if (/^kicad:/i.test(source))
18
+ return join(dir, `kicad-${KICAD_3D_TAG}`, ...kicadPath(source).split('/'));
19
+ if (/^lcsc:/i.test(source))
20
+ return join(dir, 'lcsc', `${source.slice(5).toUpperCase()}.step`);
21
+ const u = new URL(source);
22
+ return join(dir, u.host, ...u.pathname.split('/').filter(Boolean).map(decodeURIComponent));
23
+ }
24
+ /** The download URL of a `kicad:` or https source. */
25
+ export const sourceUrl = (source) => (/^kicad:/i.test(source) ? KICAD_RAW + kicadPath(source) : source);
26
+ /** The licence line of a source, as far as its host says. */
27
+ export function licenceOf(source) {
28
+ if (/^kicad:/i.test(source) || /gitlab\.com\/kicad\//.test(source))
29
+ return `KiCad 3D library ${KICAD_3D_TAG}, ${KICAD_LICENCE}`;
30
+ if (/github(usercontent)?\.com\/espressif\/kicad-libraries/.test(source))
31
+ return `Espressif KiCad library, ${KICAD_LICENCE}`;
32
+ if (/^lcsc:/i.test(source))
33
+ return 'EasyEDA library model for the LCSC part, no licence stated: for the 3D view, check the maker\'s terms before you share the file';
34
+ return 'licence not known: check the source before you share the file';
35
+ }
36
+ const LICENSES_HEAD = '# Model sources\n\nWhere each model file in this folder came from and when it was added. Check the licence of each source before you share or sell a design with the model: manufacturer models usually allow use in designs but not redistribution, and KiCad\'s packages3D models are CC-BY-SA 4.0 with an exception that lets designs and generated files use them freely.\n\n';
37
+ /** Append the source line of a model to <dir>/LICENSES.md, creating the file with its header. */
38
+ export function noteSource(dir, file, source, date = new Date().toISOString().slice(0, 10)) {
39
+ const md = join(dir, 'LICENSES.md');
40
+ const line = `- \`${file}\`: ${source}, added ${date}\n`;
41
+ const had = existsSync(md) ? readFileSync(md, 'utf8') : null;
42
+ if (had === null)
43
+ writeFileSync(md, LICENSES_HEAD + line);
44
+ else if (!had.includes(`\`${file}\`: ${source},`))
45
+ appendFileSync(md, (had.endsWith('\n') ? '' : '\n') + line);
46
+ return md;
47
+ }
48
+ /** The STEP model of an LCSC part, no login: easyeda.com/api/products/<C-number>/components names it, modules.easyeda.com serves it. */
49
+ export async function lcscModel(code, fetcher = fetch) {
50
+ const c = code.toUpperCase();
51
+ if (!/^C\d+$/.test(c))
52
+ throw new Error(`lcsc:${code}: an LCSC part number is C and digits, such as C965555`);
53
+ const api = `https://easyeda.com/api/products/${c}/components`;
54
+ const r = await fetcher(api, { headers: { accept: 'application/json' } });
55
+ const j = r.ok ? await r.json().catch(() => null) : null;
56
+ if (!j?.success || !j.result)
57
+ throw new Error(`${c}: not in EasyEDA's library (${api}: ${j?.message ?? `HTTP ${r.status}`})`);
58
+ let ds = j.result.packageDetail?.dataStr;
59
+ if (typeof ds === 'string') {
60
+ try {
61
+ ds = JSON.parse(ds);
62
+ }
63
+ catch {
64
+ ds = null;
65
+ }
66
+ }
67
+ let model = null;
68
+ for (const sh of ds?.shape ?? []) {
69
+ if (typeof sh !== 'string' || !sh.startsWith('SVGNODE~'))
70
+ continue;
71
+ try {
72
+ const a = JSON.parse(sh.slice(8)).attrs;
73
+ if (a?.c_etype === 'outline3D' && /^[0-9a-f]{32}$/.test(a.uuid)) {
74
+ model = { uuid: a.uuid, title: String(a.title ?? c) };
75
+ break;
76
+ }
77
+ }
78
+ catch { /* not a node */ }
79
+ }
80
+ const pkg = String(j.result.packageDetail?.title ?? '');
81
+ if (!model)
82
+ throw new Error(`${c}: EasyEDA's footprint ${pkg || 'for it'} has no 3D model`);
83
+ const url = EASYEDA_MODELS + model.uuid;
84
+ const m = await fetcher(url);
85
+ if (!m.ok)
86
+ throw new Error(`${c}: ${url}: HTTP ${m.status}`);
87
+ return { data: new Uint8Array(await m.arrayBuffer()), name: `${model.title}.step`, url, note: `LCSC ${c} ${j.result.title ?? ''}, EasyEDA library model ${model.title}`.replace(/\s+/g, ' ') };
88
+ }
89
+ /** Download a source into the project's cache, the file written whole or not at all, and note its licence. */
90
+ export async function download(root, source) {
91
+ const dest = fetchedPath(root, source);
92
+ let data, from;
93
+ if (/^lcsc:/i.test(source)) {
94
+ const m = await lcscModel(source.slice(5).trim());
95
+ data = m.data;
96
+ from = `${m.note}, ${m.url}`;
97
+ }
98
+ else {
99
+ const url = sourceUrl(source);
100
+ const r = await fetch(url, { redirect: 'follow' });
101
+ if (!r.ok)
102
+ throw new Error(`${url}: HTTP ${r.status}`);
103
+ data = new Uint8Array(await r.arrayBuffer());
104
+ if (!/ISO-10303-21/.test(new TextDecoder().decode(data.subarray(0, 64))))
105
+ throw new Error(`${url}: not a STEP file`);
106
+ from = url;
107
+ }
108
+ mkdirSync(dirname(dest), { recursive: true });
109
+ const tmp = `${dest}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
110
+ try {
111
+ writeFileSync(tmp, data);
112
+ renameSync(tmp, dest);
113
+ }
114
+ finally {
115
+ rmSync(tmp, { force: true });
116
+ }
117
+ const dir = fetchedDir(root);
118
+ noteSource(dir, relative(dir, dest).split(sep).join('/'), `${from}; ${licenceOf(source)}`);
119
+ return dest;
120
+ }
121
+ /** A `kicad:` source from an installed KiCad library, else null. */
122
+ function installed(source) {
123
+ if (!/^kicad:/i.test(source))
124
+ return null;
125
+ const file = kicadModel(`\${KICAD9_3DMODEL_DIR}/${kicadPath(source)}`);
126
+ return !file.startsWith('${') && existsSync(file) ? file : null;
127
+ }
128
+ const CHILD = fileURLToPath(new URL('./fetch-model-child.js', import.meta.url));
129
+ // sources that did not download in this process, and whether the network is down, so a compile tries each once
130
+ const STATE = Symbol.for('pcbjs.cli.fetchModel');
131
+ const envOffline = () => (process.env.PCBJS_OFFLINE ? 'PCBJS_OFFLINE is set' : null);
132
+ const state = () => (globalThis[STATE] ??= { failed: new Map(), offline: envOffline(), told: new Set() });
133
+ /** Forget the failures of this process, for `model fetch`. */
134
+ export function retryFetches() { const s = state(); s.failed.clear(); s.told.clear(); s.offline = envOffline(); }
135
+ /** The resolver the compile calls for each fetched model: the installed KiCad library, the cache, or a download in a child process. */
136
+ export function remoteResolver(root) {
137
+ return source => {
138
+ const local = installed(source);
139
+ if (local)
140
+ return local;
141
+ const dest = fetchedPath(root, source);
142
+ if (existsSync(dest))
143
+ return dest;
144
+ const s = state();
145
+ if (s.failed.has(source))
146
+ return null;
147
+ let why = s.offline;
148
+ if (!why) {
149
+ const r = spawnSync(process.execPath, [CHILD, root, source], { encoding: 'utf8', timeout: 60_000 });
150
+ if (existsSync(dest))
151
+ return dest;
152
+ why = (r.stderr || r.error?.message || 'the download timed out').trim().split('\n')[0];
153
+ if (r.status === 2 || r.error)
154
+ s.offline = why;
155
+ }
156
+ s.failed.set(source, why);
157
+ return null;
158
+ };
159
+ }
160
+ /** One line for the parts of a board whose models did not download, once per process for the same parts. */
161
+ export function fetchNotice(board, waiting) {
162
+ if (!waiting.length)
163
+ return null;
164
+ const s = state(), refs = waiting.map(w => w.ref);
165
+ const key = `${board}:${refs.join(',')}`;
166
+ if (s.told.has(key))
167
+ return null;
168
+ s.told.add(key);
169
+ const why = [...new Set(waiting.map(w => s.failed.get(w.source)).filter(Boolean))].join('; ') || 'not fetched';
170
+ const list = refs.length > 6 ? `${refs.slice(0, 6).join(', ')} and ${refs.length - 6} more` : refs.join(', ');
171
+ return `pcbjs: ${board}: ${list} ${refs.length === 1 ? 'shows a dashed outline' : 'show dashed outlines'}, the model did not download (${why}); run pcbjs model fetch`;
172
+ }
173
+ /** Let the next compile of this project resolve and fetch its models; the compile is synchronous, so each sets its own root. */
174
+ export function installModelFetch(root) {
175
+ ;
176
+ globalThis[REMOTE_MODEL] = remoteResolver(root);
177
+ }
@@ -0,0 +1,60 @@
1
+ /** Additive, the same number as @pcbjs/lsp's PROTOCOL. */
2
+ export declare const IDE_PROTOCOL = 2;
3
+ /** Bumped only by a change an older peer cannot survive. */
4
+ export declare const IDE_MAJOR = 1;
5
+ export interface HostHello {
6
+ name: string;
7
+ version: string;
8
+ protocol: number;
9
+ major: number;
10
+ capabilities: string[] | null;
11
+ }
12
+ export interface HostCursor {
13
+ file: string;
14
+ line: number;
15
+ col: number;
16
+ }
17
+ export interface HostFocus {
18
+ file: string;
19
+ }
20
+ export interface HostClientOptions {
21
+ /** `ws://127.0.0.1:<port>/<token>`, already checked with parseIdeUrl */
22
+ url: string;
23
+ onHello?(hello: HostHello): void;
24
+ onCursor?(cursor: HostCursor): void;
25
+ onFocus?(focus: HostFocus): void;
26
+ onClose?(close: HostFocus): void;
27
+ onBye?(): void;
28
+ onRpc?(msg: unknown): void;
29
+ /** once per connection and once per run of failed attempts */
30
+ onStatus?(status: 'connected' | 'disconnected' | 'unreachable', detail?: string): void;
31
+ /** ms, doubled per failed attempt up to maxDelay */
32
+ minDelay?: number;
33
+ maxDelay?: number;
34
+ }
35
+ export interface HostClient {
36
+ /** False while disconnected; STICKY events are sent again on every reconnect. */
37
+ send(event: string, data: unknown): boolean;
38
+ sendRpc(msg: unknown): boolean;
39
+ connected(): boolean;
40
+ hello(): HostHello | null;
41
+ close(): void;
42
+ }
43
+ export declare const STICKY: readonly ["pcbjs:ide:ready", "pcbjs:ide:scopes", "pcbjs:ide:scope", "pcbjs:ide:boards"];
44
+ /** Throws a message for the CLI unless ws or wss and loopback, or allowRemote. */
45
+ export declare function parseIdeUrl(url: string, allowRemote?: boolean): URL;
46
+ /** Without path and query, so the token never reaches the terminal. */
47
+ export declare const redactUrl: (url: string) => string;
48
+ /** What `ready` offers: the events it takes, and `lsp` when @pcbjs/lsp resolves. */
49
+ export declare function serverCapabilities(): string[];
50
+ export declare const readyProtocol: () => {
51
+ protocol: number;
52
+ major: number;
53
+ capabilities: string[];
54
+ };
55
+ /** A hello without `protocol` comes from a protocol 1 editor. */
56
+ export declare function parseHello(d: any): HostHello;
57
+ /** Null unless the editor's protocol major is older than this server's. */
58
+ export declare function helloWarning(h: HostHello): string | null;
59
+ /** Keeps dialling with a backoff until close(). */
60
+ export declare function connectHost(opts: HostClientOptions): HostClient;