@metreeca/rack 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/README.md CHANGED
@@ -10,10 +10,13 @@ package consumes a single maintained copy instead of carrying its own. Tools are
10
10
  | Subpath | Contents |
11
11
  |-----------------------------|--------------|
12
12
  | [@metreeca/rack/tool][tool] | CLI tools |
13
+ | [@metreeca/rack/node][node] | Node hooks |
13
14
  | [@metreeca/rack/vite][vite] | Vite plugins |
14
15
 
15
16
  [tool]: https://metreeca.github.io/rack/modules/tool.html
16
17
 
18
+ [node]: https://metreeca.github.io/rack/modules/node.html
19
+
17
20
  [vite]: https://metreeca.github.io/rack/modules/vite.html
18
21
 
19
22
  # Installation
@@ -0,0 +1,3 @@
1
+ import { customConditions, symlinks } from "./symlinks.js";
2
+ export { symlinks, customConditions };
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAoCA,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,38 @@
1
+ /*
2
+ * Copyright © 2026 Metreeca srl
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Node module hooks running a process against the live source of locally linked packages, with no rebuild.
18
+ *
19
+ * Importing this module registers the {@link symlinks} hooks for the rest of the process, with the `customConditions`
20
+ * of the `tsconfig.json` nearest to the current working directory. Node then resolves locally linked packages to the
21
+ * same sources the IDE does. The module must load before any module it resolves, through the `--import` option given
22
+ * on the command line, in an npm script or in the Node.js run configuration template of the IDE:
23
+ *
24
+ * ```shell
25
+ * node --import @metreeca/rack/node src/index.ts
26
+ * ```
27
+ *
28
+ * Plain `node` runs TypeScript sources through its built-in type stripping, which supports erasable syntax only.
29
+ * Sources using `enum`, `namespace` or parameter properties need a loader such as `tsx` registered ahead of the
30
+ * hooks, as in `node --import tsx --import @metreeca/rack/node` or `tsx --import @metreeca/rack/node`.
31
+ *
32
+ * @module
33
+ */
34
+ import { registerHooks } from "node:module";
35
+ import { customConditions, symlinks } from "./symlinks.js";
36
+ export { symlinks, customConditions };
37
+ registerHooks(symlinks({ conditions: customConditions(process.cwd()) }));
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/node/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE3D,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;AAGtC,aAAa,CAAC,QAAQ,CAAC,EAAE,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC"}
@@ -0,0 +1,54 @@
1
+ import type { ResolveHookSync } from "node:module";
2
+ /**
3
+ * Creates module hooks running a Node process against the live source of locally linked packages, with no rebuild.
4
+ *
5
+ * Imports of workspace members and of sibling checkouts linked into `node_modules` resolve to their TypeScript source,
6
+ * which Node runs through its built-in type stripping, so edits are picked up as they are saved. Packages installed
7
+ * from the registry keep resolving to their build, as do packages whose `exports` name none of the conditions.
8
+ *
9
+ * Packages are looked up as Node does, from the `node_modules` nearest to the importer up through its ancestors;
10
+ * imports with no importer start from the root. A linked checkout importing a package the project links as well
11
+ * loads the linked source rather than the registry copy it installs for itself, so the two never diverge.
12
+ *
13
+ * The process loads a single copy of every linked package and of each of their `peerDependencies`, as libraries
14
+ * holding module state such as Preact hooks require. Those packages resolve from the root, whatever copies a linked
15
+ * checkout installs for itself, and from the importer only where the root does not install them. Other dependencies
16
+ * of a linked package resolve from its checkout, as Node does, since the process loads no other copy of them.
17
+ *
18
+ * Relative imports of a `.js` file from a TypeScript module, as `NodeNext` module resolution requires, resolve to the
19
+ * `.ts` file next to it when the `.js` file does not exist. Sources compiled to `dist` thus run from `src` under plain
20
+ * `node`, with no loader such as `tsx`. Where such a loader is registered as well, the imports it resolves are left
21
+ * to it, and the hooks only add the resolution of linked packages to their TypeScript source.
22
+ *
23
+ * The hooks take effect once registered with `registerHooks` from `node:module`, before the first import they have
24
+ * to resolve, typically from a module loaded with `--import`. Importing `@metreeca/rack/node` registers them with the
25
+ * conditions of the `tsconfig.json` nearest to the current working directory, with no setup module to write.
26
+ *
27
+ * @param options The resolution settings
28
+ *
29
+ * @returns Synchronous module hooks resolving linked imports to source, deduplicating linked packages and their
30
+ * peers, mapping `.js` relative imports to their `.ts` sources, and leaving all other imports to the default resolution
31
+ */
32
+ export declare function symlinks({ conditions, root }: {
33
+ /**
34
+ * The export conditions mapping each subpath to its source file, as listed in `customConditions`; where a package
35
+ * matches more than one, the key order of its own `exports` decides which applies.
36
+ */
37
+ conditions: string | readonly string[];
38
+ /**
39
+ * The project root the linked packages and their peers resolve from; defaults to the current working directory.
40
+ */
41
+ root?: string;
42
+ }): Readonly<{
43
+ resolve: ResolveHookSync;
44
+ }>;
45
+ /**
46
+ * Reads the custom export conditions a project asks its TypeScript language service to resolve.
47
+ *
48
+ * @param directory The directory the search for `tsconfig.json` starts from, walking up through its ancestors
49
+ *
50
+ * @returns The `customConditions` of the nearest `tsconfig.json`, read through its `extends` chain where it declares
51
+ * none of its own; empty if no configuration is found or none declares any, so that linked packages resolve to `dist`
52
+ */
53
+ export declare function customConditions(directory: string): readonly string[];
54
+ //# sourceMappingURL=symlinks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symlinks.d.ts","sourceRoot":"","sources":["../../src/node/symlinks.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAOnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,QAAQ,CAAC,EAExB,UAAU,EACV,IAAoB,EAEpB,EAAE;IAEF;;;OAGG;IACH,UAAU,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAAA;IAEtC;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;CAEb,GAAG,QAAQ,CAAC;IAAE,OAAO,EAAE,eAAe,CAAA;CAAE,CAAC,CAgGzC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CA2BrE"}
@@ -0,0 +1,134 @@
1
+ /*
2
+ * Copyright © 2026 Metreeca srl
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { existsSync, readFileSync, realpathSync } from "node:fs";
17
+ import { dirname, join, resolve } from "node:path";
18
+ import { fileURLToPath, pathToFileURL } from "node:url";
19
+ import { exports } from "resolve.exports";
20
+ import { bare, lookup, manifest, shared } from "../symlinks.js";
21
+ /**
22
+ * Creates module hooks running a Node process against the live source of locally linked packages, with no rebuild.
23
+ *
24
+ * Imports of workspace members and of sibling checkouts linked into `node_modules` resolve to their TypeScript source,
25
+ * which Node runs through its built-in type stripping, so edits are picked up as they are saved. Packages installed
26
+ * from the registry keep resolving to their build, as do packages whose `exports` name none of the conditions.
27
+ *
28
+ * Packages are looked up as Node does, from the `node_modules` nearest to the importer up through its ancestors;
29
+ * imports with no importer start from the root. A linked checkout importing a package the project links as well
30
+ * loads the linked source rather than the registry copy it installs for itself, so the two never diverge.
31
+ *
32
+ * The process loads a single copy of every linked package and of each of their `peerDependencies`, as libraries
33
+ * holding module state such as Preact hooks require. Those packages resolve from the root, whatever copies a linked
34
+ * checkout installs for itself, and from the importer only where the root does not install them. Other dependencies
35
+ * of a linked package resolve from its checkout, as Node does, since the process loads no other copy of them.
36
+ *
37
+ * Relative imports of a `.js` file from a TypeScript module, as `NodeNext` module resolution requires, resolve to the
38
+ * `.ts` file next to it when the `.js` file does not exist. Sources compiled to `dist` thus run from `src` under plain
39
+ * `node`, with no loader such as `tsx`. Where such a loader is registered as well, the imports it resolves are left
40
+ * to it, and the hooks only add the resolution of linked packages to their TypeScript source.
41
+ *
42
+ * The hooks take effect once registered with `registerHooks` from `node:module`, before the first import they have
43
+ * to resolve, typically from a module loaded with `--import`. Importing `@metreeca/rack/node` registers them with the
44
+ * conditions of the `tsconfig.json` nearest to the current working directory, with no setup module to write.
45
+ *
46
+ * @param options The resolution settings
47
+ *
48
+ * @returns Synchronous module hooks resolving linked imports to source, deduplicating linked packages and their
49
+ * peers, mapping `.js` relative imports to their `.ts` sources, and leaving all other imports to the default resolution
50
+ */
51
+ export function symlinks({ conditions, root = process.cwd() }) {
52
+ const $conditions = typeof conditions === "string" ? [conditions] : conditions;
53
+ const names = new Set(shared(root)); // the packages resolved from the root, whoever imports them
54
+ return {
55
+ resolve(specifier, context, next) {
56
+ const { parentURL } = context;
57
+ const parent = parentURL?.startsWith("file:") ? fileURLToPath(parentURL) : undefined;
58
+ const base = parent === undefined ? root : dirname(parent);
59
+ const name = bare(specifier);
60
+ const link = name === undefined ? undefined : lookup(name, base) ?? lookup(name, root);
61
+ if (link !== undefined) {
62
+ const path = realpathSync(link);
63
+ const target = source(path, specifier, context.conditions);
64
+ return target === undefined
65
+ ? next(specifier, context) // subpath not exported or excluded by a null target: node reports it
66
+ : { url: pathToFileURL(resolve(path, target)).href, shortCircuit: true };
67
+ }
68
+ else if (name !== undefined && names.has(name)) {
69
+ return rooted(specifier, context, next);
70
+ }
71
+ else if (parent?.endsWith(".ts") && /^\.\.?\/.*\.js$/.test(specifier)) {
72
+ return sibling(specifier, context, next, resolve(base, specifier).replace(/\.js$/, ".ts"));
73
+ }
74
+ else {
75
+ return next(specifier, context);
76
+ }
77
+ }
78
+ };
79
+ function source(path, specifier, conditions) {
80
+ try {
81
+ return exports(manifest(path), specifier, {
82
+ conditions: $conditions,
83
+ require: conditions.includes("require")
84
+ })?.[0];
85
+ }
86
+ catch { // subpath not exported or excluded by a null target
87
+ return undefined;
88
+ }
89
+ }
90
+ function rooted(specifier, context, next) {
91
+ try {
92
+ return next(specifier, { ...context, parentURL: pathToFileURL(join(root, "package.json")).href });
93
+ }
94
+ catch { // not installed by the root: falls back to the importer
95
+ return next(specifier, context);
96
+ }
97
+ }
98
+ function sibling(specifier, context, next, path) {
99
+ try {
100
+ return next(specifier, context);
101
+ }
102
+ catch (error) { // the .js file does not exist: a .ts file next to it is the source
103
+ if (existsSync(path)) {
104
+ return { url: pathToFileURL(path).href, shortCircuit: true };
105
+ }
106
+ else {
107
+ throw error;
108
+ }
109
+ }
110
+ }
111
+ }
112
+ /**
113
+ * Reads the custom export conditions a project asks its TypeScript language service to resolve.
114
+ *
115
+ * @param directory The directory the search for `tsconfig.json` starts from, walking up through its ancestors
116
+ *
117
+ * @returns The `customConditions` of the nearest `tsconfig.json`, read through its `extends` chain where it declares
118
+ * none of its own; empty if no configuration is found or none declares any, so that linked packages resolve to `dist`
119
+ */
120
+ export function customConditions(directory) {
121
+ const config = join(directory, "tsconfig.json");
122
+ const parent = dirname(directory);
123
+ return existsSync(config) ? declared(config)
124
+ : parent === directory ? []
125
+ : customConditions(parent);
126
+ function declared(config) {
127
+ const { extends: base, compilerOptions } = JSON.parse(readFileSync(config, "utf8")); // trusted local file, not validated
128
+ const conditions = compilerOptions?.customConditions;
129
+ return conditions !== undefined ? conditions
130
+ : base !== undefined && existsSync(resolve(dirname(config), base)) ? declared(resolve(dirname(config), base))
131
+ : [];
132
+ }
133
+ }
134
+ //# sourceMappingURL=symlinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symlinks.js","sourceRoot":"","sources":["../../src/node/symlinks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import type { Package } from "resolve.exports";
2
+ /**
3
+ * The package manifest fields the linked package resolvers and the `ln` command rely on.
4
+ */
5
+ export type Manifest = Package & Readonly<{
6
+ bin?: string | Readonly<Record<string, string>>;
7
+ workspaces?: readonly string[];
8
+ dependencies?: Readonly<Record<string, string>>;
9
+ devDependencies?: Readonly<Record<string, string>>;
10
+ peerDependencies?: Readonly<Record<string, string>>;
11
+ optionalDependencies?: Readonly<Record<string, string>>;
12
+ }>;
13
+ /**
14
+ * Matches the package name a module specifier starts with.
15
+ *
16
+ * @param specifier The module specifier to match
17
+ *
18
+ * @returns The scoped or bare package name opening `specifier`, subpath excluded; `undefined` for relative and
19
+ * absolute paths, for URLs and for `node:` builtins, which no `node_modules` lookup can resolve
20
+ */
21
+ export declare function bare(specifier: string): string | undefined;
22
+ /**
23
+ * Looks a package up as Node does, from the `node_modules` nearest to a directory up through its ancestors.
24
+ *
25
+ * @param name The name of the package to look up
26
+ * @param directory The directory the lookup starts from
27
+ *
28
+ * @returns The path of the `node_modules` entry of `name`, if it is a symbolic link; `undefined` if the package is
29
+ * not found or is an installed copy, which the default resolution handles as it does for any registry package
30
+ */
31
+ export declare function lookup(name: string, directory: string): string | undefined;
32
+ /**
33
+ * Lists the packages a project loads a single copy of.
34
+ *
35
+ * @param root The project root
36
+ *
37
+ * @returns The names of the packages linked into the `node_modules` of `root`, of its ancestors and of its workspace
38
+ * members, along with the names of their `peerDependencies`, without duplicates
39
+ */
40
+ export declare function shared(root: string): readonly string[];
41
+ /**
42
+ * Reads a package manifest.
43
+ *
44
+ * @param path The path of the package folder
45
+ *
46
+ * @returns The manifest of the package at `path`, read once per process for each distinct `path`
47
+ */
48
+ export declare function manifest(path: string): Manifest;
49
+ //# sourceMappingURL=symlinks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symlinks.d.ts","sourceRoot":"","sources":["../src/symlinks.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAW/C;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,QAAQ,CAAC;IAEzC,GAAG,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC/C,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAE9B,YAAY,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAC/C,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IAClD,gBAAgB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACnD,oBAAoB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAEvD,CAAC,CAAC;AAKH;;;;;;;GAOG;AACH,wBAAgB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE1D;AAED;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAQ1E;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CA6CtD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAkB/C"}
@@ -0,0 +1,107 @@
1
+ /*
2
+ * Copyright © 2026 Metreeca srl
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ import { existsSync, globSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
17
+ import { createRequire } from "node:module";
18
+ import { join, parse, relative, sep } from "node:path";
19
+ /**
20
+ * The package manifests read by {@link manifest}, keyed by package folder path and retained for the process lifetime.
21
+ */
22
+ const manifests = new Map();
23
+ ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
24
+ /**
25
+ * Matches the package name a module specifier starts with.
26
+ *
27
+ * @param specifier The module specifier to match
28
+ *
29
+ * @returns The scoped or bare package name opening `specifier`, subpath excluded; `undefined` for relative and
30
+ * absolute paths, for URLs and for `node:` builtins, which no `node_modules` lookup can resolve
31
+ */
32
+ export function bare(specifier) {
33
+ return specifier.match(/^(?![./]|[a-z]+:)(@[^/]+\/)?[^/]+/i)?.[0];
34
+ }
35
+ /**
36
+ * Looks a package up as Node does, from the `node_modules` nearest to a directory up through its ancestors.
37
+ *
38
+ * @param name The name of the package to look up
39
+ * @param directory The directory the lookup starts from
40
+ *
41
+ * @returns The path of the `node_modules` entry of `name`, if it is a symbolic link; `undefined` if the package is
42
+ * not found or is an installed copy, which the default resolution handles as it does for any registry package
43
+ */
44
+ export function lookup(name, directory) {
45
+ const path = (createRequire(join(directory, sep)).resolve.paths(name) ?? []) // trailing separator: a folder, not a file
46
+ .map(modules => join(modules, name))
47
+ .find(candidate => lstatSync(candidate, { throwIfNoEntry: false }));
48
+ return path !== undefined && lstatSync(path).isSymbolicLink() ? path : undefined;
49
+ }
50
+ /**
51
+ * Lists the packages a project loads a single copy of.
52
+ *
53
+ * @param root The project root
54
+ *
55
+ * @returns The names of the packages linked into the `node_modules` of `root`, of its ancestors and of its workspace
56
+ * members, along with the names of their `peerDependencies`, without duplicates
57
+ */
58
+ export function shared(root) {
59
+ return [...new Set(links().flatMap(link => {
60
+ const { name, peerDependencies } = manifest(realpathSync(link));
61
+ return [name, ...Object.keys(peerDependencies ?? {})];
62
+ }))];
63
+ function links() {
64
+ return [...ancestors(), ...members()]
65
+ .map(directory => join(directory, "node_modules"))
66
+ .flatMap(modules => children(modules)
67
+ .filter(entry => !entry.startsWith(".")) // hidden entries such as .bin
68
+ .flatMap(entry => entry.startsWith("@")
69
+ ? children(join(modules, entry)).map(name => join(modules, entry, name))
70
+ : [join(modules, entry)])
71
+ .filter(entry => lstatSync(entry).isSymbolicLink()));
72
+ }
73
+ function ancestors() {
74
+ const { root: base } = parse(root);
75
+ const segments = relative(base, root).split(sep).filter(Boolean);
76
+ return segments.map((_, index) => join(base, ...segments.slice(0, segments.length - index)));
77
+ }
78
+ function members() {
79
+ return existsSync(join(root, "package.json"))
80
+ ? (manifest(root).workspaces ?? [])
81
+ .flatMap(pattern => globSync(pattern, { cwd: root }))
82
+ .map(member => join(root, member))
83
+ : [];
84
+ }
85
+ function children(path) {
86
+ return statSync(path, { throwIfNoEntry: false })?.isDirectory() ? readdirSync(path) : [];
87
+ }
88
+ }
89
+ /**
90
+ * Reads a package manifest.
91
+ *
92
+ * @param path The path of the package folder
93
+ *
94
+ * @returns The manifest of the package at `path`, read once per process for each distinct `path`
95
+ */
96
+ export function manifest(path) {
97
+ const cached = manifests.get(path);
98
+ if (cached === undefined) {
99
+ const loaded = JSON.parse(readFileSync(join(path, "package.json"), "utf8")); // trusted local file
100
+ manifests.set(path, loaded);
101
+ return loaded;
102
+ }
103
+ else {
104
+ return cached;
105
+ }
106
+ }
107
+ //# sourceMappingURL=symlinks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symlinks.js","sourceRoot":"","sources":["../src/symlinks.ts"],"names":[],"mappings":"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"}
package/dist/tool/cp.d.ts CHANGED
@@ -1,21 +1,22 @@
1
- #!/usr/bin/env node
2
1
  /**
3
2
  * Copies the files matching a set of glob patterns into a target directory, keeping their relative layout.
4
3
  *
4
+ * Runs on the command line as `ush cp <target> <pattern>...`.
5
+ *
5
6
  * Each match lands under `target` at its path relative to the base of its pattern, that is the leading segments free
6
7
  * of glob characters: `src/**\/*.css` copies `src/a/b.css` to `<target>/a/b.css`, and a plain file path copies the
7
8
  * file to the top of `target`. Missing directories are created and existing files are overwritten.
8
9
  *
9
10
  * Directories matched by a pattern are skipped: only files are copied.
10
11
  *
11
- * Each copied file is printed on stdout, one per line, by its source path as the pattern matched it.
12
+ * Each copied file is printed on stdout as `cp <source>`, with the source path spelled as the pattern matched it.
12
13
  *
13
14
  * @example
14
15
  *
15
16
  * Shipping stylesheets next to the compiled modules, from an npm script:
16
17
  *
17
18
  * ```shell
18
- * cp dist 'src/**\/*.css'
19
+ * ush cp dist 'src/**\/*.css'
19
20
  * ```
20
21
  *
21
22
  * Patterns are quoted so that they reach the tool unexpanded, as shells differ in their glob support.
@@ -27,4 +28,7 @@
27
28
  * @throws Error If a pattern matches no file, in which case no file is copied for any of the patterns
28
29
  */
29
30
  export declare function cp(target: string, ...patterns: readonly string[]): void;
31
+ export declare namespace cp {
32
+ var usage: string;
33
+ }
30
34
  //# sourceMappingURL=cp.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cp.d.ts","sourceRoot":"","sources":["../../src/tool/cp.ts"],"names":[],"mappings":";AA8BA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAiCvE"}
1
+ {"version":3,"file":"cp.d.ts","sourceRoot":"","sources":["../../src/tool/cp.ts"],"names":[],"mappings":"AA4BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAiCvE;yBAjCe,EAAE"}
package/dist/tool/cp.js CHANGED
@@ -1,4 +1,3 @@
1
- #!/usr/bin/env node
2
1
  /*
3
2
  * Copyright © 2026 Metreeca srl
4
3
  *
@@ -16,28 +15,30 @@
16
15
  */
17
16
  import { copyFileSync, globSync, mkdirSync, statSync } from "node:fs";
18
17
  import { dirname, join, relative } from "node:path";
19
- import { cli } from "./index.core.js";
20
- if (import.meta.main) {
21
- process.exitCode = cli(cp, "<target> <pattern>...");
22
- }
18
+ /**
19
+ * The command line synopsis of {@link cp}, printed by `ush cp --help`.
20
+ */
21
+ cp.usage = "<target> <pattern>...";
23
22
  ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
24
23
  /**
25
24
  * Copies the files matching a set of glob patterns into a target directory, keeping their relative layout.
26
25
  *
26
+ * Runs on the command line as `ush cp <target> <pattern>...`.
27
+ *
27
28
  * Each match lands under `target` at its path relative to the base of its pattern, that is the leading segments free
28
29
  * of glob characters: `src/**\/*.css` copies `src/a/b.css` to `<target>/a/b.css`, and a plain file path copies the
29
30
  * file to the top of `target`. Missing directories are created and existing files are overwritten.
30
31
  *
31
32
  * Directories matched by a pattern are skipped: only files are copied.
32
33
  *
33
- * Each copied file is printed on stdout, one per line, by its source path as the pattern matched it.
34
+ * Each copied file is printed on stdout as `cp <source>`, with the source path spelled as the pattern matched it.
34
35
  *
35
36
  * @example
36
37
  *
37
38
  * Shipping stylesheets next to the compiled modules, from an npm script:
38
39
  *
39
40
  * ```shell
40
- * cp dist 'src/**\/*.css'
41
+ * ush cp dist 'src/**\/*.css'
41
42
  * ```
42
43
  *
43
44
  * Patterns are quoted so that they reach the tool unexpanded, as shells differ in their glob support.
@@ -58,7 +59,7 @@ export function cp(target, ...patterns) {
58
59
  patterns.flatMap(copies).forEach(({ source, destination }) => {
59
60
  mkdirSync(dirname(destination), { recursive: true });
60
61
  copyFileSync(source, destination);
61
- console.log(source);
62
+ console.log(`cp ${source}`);
62
63
  });
63
64
  function copies(pattern) {
64
65
  const sources = globSync(pattern).filter(source => statSync(source).isFile());
@@ -1 +1 @@
1
- {"version":3,"file":"cp.js","sourceRoot":"","sources":["../../src/tool/cp.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,GAAG,EAAE,MAAM,iBAAiB,CAAC;AAGtC,IAAK,MAAM,CAAC,IAAI,CAAC,IAAI,EAAG,CAAC;IACxB,OAAO,CAAC,QAAQ,GAAG,GAAG,CAAC,EAAE,EAAE,uBAAuB,CAAC,CAAC;AACrD,CAAC;AAGD,wHAAwH;AAExH;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,EAAE,CAAC,MAAc,EAAE,GAAG,QAA2B;IAEhE,IAAK,CAAC,MAAM,EAAG,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,wBAAwB,CAAC,CAAC;IAC/C,CAAC;IAED,IAAK,CAAC,QAAQ,CAAC,MAAM,EAAG,CAAC;QACxB,MAAM,IAAI,SAAS,CAAC,yBAAyB,CAAC,CAAC;IAChD,CAAC;IAED,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE;QAC5D,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,YAAY,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;IAGH,SAAS,MAAM,CAAC,OAAe;QAE9B,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QAE9E,IAAK,CAAC,OAAO,CAAC,MAAM,EAAG,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,sBAAsB,OAAO,GAAG,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QACtE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;QAEpF,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE/F,CAAC;AAEF,CAAC"}
1
+ {"version":3,"file":"cp.js","sourceRoot":"","sources":["../../src/tool/cp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACtE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAGpD;;GAEG;AACH,EAAE,CAAC,KAAK,GAAG,uBAAuB,CAAC;AAGnC,wHAAwH;AAExH;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,EAAE,CAAC,MAAc,EAAE,GAAG,QAA2B;IAEhE,IAAK,CAAC,MAAM,EAAG,CAAC;QACf,MAAM,IAAI,SAAS,CAAC,wBAAwB,CAAC,CAAC;IAC/C,CAAC;IAED,IAAK,CAAC,QAAQ,CAAC,MAAM,EAAG,CAAC;QACxB,MAAM,IAAI,SAAS,CAAC,yBAAyB,CAAC,CAAC;IAChD,CAAC;IAED,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,EAAE,EAAE;QAC5D,SAAS,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrD,YAAY,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAClC,OAAO,CAAC,GAAG,CAAC,MAAM,MAAM,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAGH,SAAS,MAAM,CAAC,OAAe;QAE9B,MAAM,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QAE9E,IAAK,CAAC,OAAO,CAAC,MAAM,EAAG,CAAC;YACvB,MAAM,IAAI,KAAK,CAAC,sBAAsB,OAAO,GAAG,CAAC,CAAC;QACnD,CAAC;QAED,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QACtE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;QAEpF,OAAO,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAE/F,CAAC;AAEF,CAAC"}
@@ -4,15 +4,17 @@
4
4
  * The tool receives the operands as its arguments, with `undefined` in place of a missing one, and throws a
5
5
  * `TypeError` on a command line it cannot act on, so that the error message also points to the help.
6
6
  *
7
- * `--help` and `-h` print the usage line to stdout and skip the tool. Errors go to stderr prefixed with the tool name;
8
- * usage errors, including unrecognized options, also point to `--help`. Operands starting with `-` follow `--`.
7
+ * `--help` and `-h` print the usage line to stdout, followed by the description if any, and skip the tool. Errors go to
8
+ * stderr prefixed with the command name; usage errors, including unrecognized options, also point to `--help`.
9
+ * Operands starting with `-` follow `--`, which ends option parsing as on any GNU command line.
9
10
  *
10
11
  * @param tool The tool to run on the operands
11
- * @param help The operand synopsis printed by `--help` after the tool name, such as `<target> <pattern>...`
12
- * @param name The tool name prefixing messages; defaults to the script name without extension, else the `tool` name
13
- * @param args The command line arguments after the script path; defaults to those the process was launched with
12
+ * @param help The operand synopsis printed by `--help` after the command name, such as `<target> <pattern>...`
13
+ * @param name The command prefixing messages, as in `ush cp`; defaults to the bare script name, else the `tool` name
14
+ * @param args The command line arguments after the command name; defaults to those the process was launched with
15
+ * @param description The text printed by `--help` below the usage line, such as a list of subcommands
14
16
  *
15
17
  * @returns The process exit status: `0` on success, `1` on failure
16
18
  */
17
- export declare function cli(tool: (...args: string[]) => void, help: string, name?: string, args?: readonly string[]): number;
19
+ export declare function cli(tool: (...args: string[]) => void, help: string, name?: string, args?: readonly string[], description?: string): number;
18
20
  //# sourceMappingURL=index.core.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.core.d.ts","sourceRoot":"","sources":["../../src/tool/index.core.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,GAAG,CAClB,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,EACjC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAgF,EACtF,IAAI,GAAE,SAAS,MAAM,EAA0B,GAC7C,MAAM,CAoDR"}
1
+ {"version":3,"file":"index.core.d.ts","sourceRoot":"","sources":["../../src/tool/index.core.ts"],"names":[],"mappings":"AAoBA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,GAAG,CAClB,IAAI,EAAE,CAAC,GAAG,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,EACjC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAgF,EACtF,IAAI,GAAE,SAAS,MAAM,EAA0B,EAC/C,WAAW,CAAC,EAAE,MAAM,GAClB,MAAM,CAuDR"}
@@ -21,17 +21,19 @@ import { parseArgs } from "node:util";
21
21
  * The tool receives the operands as its arguments, with `undefined` in place of a missing one, and throws a
22
22
  * `TypeError` on a command line it cannot act on, so that the error message also points to the help.
23
23
  *
24
- * `--help` and `-h` print the usage line to stdout and skip the tool. Errors go to stderr prefixed with the tool name;
25
- * usage errors, including unrecognized options, also point to `--help`. Operands starting with `-` follow `--`.
24
+ * `--help` and `-h` print the usage line to stdout, followed by the description if any, and skip the tool. Errors go to
25
+ * stderr prefixed with the command name; usage errors, including unrecognized options, also point to `--help`.
26
+ * Operands starting with `-` follow `--`, which ends option parsing as on any GNU command line.
26
27
  *
27
28
  * @param tool The tool to run on the operands
28
- * @param help The operand synopsis printed by `--help` after the tool name, such as `<target> <pattern>...`
29
- * @param name The tool name prefixing messages; defaults to the script name without extension, else the `tool` name
30
- * @param args The command line arguments after the script path; defaults to those the process was launched with
29
+ * @param help The operand synopsis printed by `--help` after the command name, such as `<target> <pattern>...`
30
+ * @param name The command prefixing messages, as in `ush cp`; defaults to the bare script name, else the `tool` name
31
+ * @param args The command line arguments after the command name; defaults to those the process was launched with
32
+ * @param description The text printed by `--help` below the usage line, such as a list of subcommands
31
33
  *
32
34
  * @returns The process exit status: `0` on success, `1` on failure
33
35
  */
34
- export function cli(tool, help, name = process.argv[1] !== undefined ? parse(process.argv[1]).name : tool.name, args = process.argv.slice(2)) {
36
+ export function cli(tool, help, name = process.argv[1] !== undefined ? parse(process.argv[1]).name : tool.name, args = process.argv.slice(2), description) {
35
37
  const { values, positionals, tokens } = parseArgs({
36
38
  args: [...args],
37
39
  options: { help: { type: "boolean", short: "h" } },
@@ -44,7 +46,9 @@ export function cli(tool, help, name = process.argv[1] !== undefined ? parse(pro
44
46
  return fail(`unrecognized option <${unknown.rawName}>`, true);
45
47
  }
46
48
  else if (values.help) {
47
- console.log(`Usage: ${name} ${help}`);
49
+ console.log(description === undefined
50
+ ? `Usage: ${name} ${help}`
51
+ : `Usage: ${name} ${help}\n\n${description}`);
48
52
  return 0;
49
53
  }
50
54
  else {
@@ -1 +1 @@
1
- {"version":3,"file":"index.core.js","sourceRoot":"","sources":["../../src/tool/index.core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,GAAG,CAClB,IAAiC,EACjC,IAAY,EACZ,OAAe,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EACtF,OAA0B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAG/C,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QACjD,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAClD,gBAAgB,EAAE,IAAI;QACtB,MAAM,EAAE,KAAK,EAAE,+DAA+D;QAC9E,MAAM,EAAE,IAAI;KACZ,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CACnC,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAChD,CAAC;IAEF,IAAK,OAAO,EAAE,IAAI,KAAK,QAAQ,EAAG,CAAC;QAElC,OAAO,IAAI,CAAC,wBAAwB,OAAO,CAAC,OAAO,GAAG,EAAE,IAAI,CAAC,CAAC;IAE/D,CAAC;SAAM,IAAK,MAAM,CAAC,IAAI,EAAG,CAAC;QAE1B,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,IAAI,EAAE,CAAC,CAAC;QAEtC,OAAO,CAAC,CAAC;IAEV,CAAC;SAAM,CAAC;QAEP,IAAI,CAAC;YAEJ,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC;YAErB,OAAO,CAAC,CAAC;QAEV,CAAC;QAAC,OAAQ,KAAK,EAAG,CAAC;YAElB,OAAO,IAAI,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,YAAY,SAAS,CAAC,CAAC;QAEjG,CAAC;IAEF,CAAC;IAGD,SAAS,IAAI,CAAC,OAAe,EAAE,KAAc;QAE5C,OAAO,CAAC,KAAK,CAAC,KAAK;YAClB,CAAC,CAAC,GAAG,IAAI,KAAK,OAAO,UAAU,IAAI,gCAAgC;YACnE,CAAC,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CACvB,CAAC;QAEF,OAAO,CAAC,CAAC;IAEV,CAAC;AAEF,CAAC"}
1
+ {"version":3,"file":"index.core.js","sourceRoot":"","sources":["../../src/tool/index.core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAGtC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,GAAG,CAClB,IAAiC,EACjC,IAAY,EACZ,OAAe,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EACtF,OAA0B,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAC/C,WAAoB;IAGpB,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC;QACjD,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QACf,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAClD,gBAAgB,EAAE,IAAI;QACtB,MAAM,EAAE,KAAK,EAAE,+DAA+D;QAC9E,MAAM,EAAE,IAAI;KACZ,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CACnC,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,KAAK,MAAM,CAChD,CAAC;IAEF,IAAK,OAAO,EAAE,IAAI,KAAK,QAAQ,EAAG,CAAC;QAElC,OAAO,IAAI,CAAC,wBAAwB,OAAO,CAAC,OAAO,GAAG,EAAE,IAAI,CAAC,CAAC;IAE/D,CAAC;SAAM,IAAK,MAAM,CAAC,IAAI,EAAG,CAAC;QAE1B,OAAO,CAAC,GAAG,CAAC,WAAW,KAAK,SAAS;YACpC,CAAC,CAAC,UAAU,IAAI,IAAI,IAAI,EAAE;YAC1B,CAAC,CAAC,UAAU,IAAI,IAAI,IAAI,OAAO,WAAW,EAAE,CAC5C,CAAC;QAEF,OAAO,CAAC,CAAC;IAEV,CAAC;SAAM,CAAC;QAEP,IAAI,CAAC;YAEJ,IAAI,CAAC,GAAG,WAAW,CAAC,CAAC;YAErB,OAAO,CAAC,CAAC;QAEV,CAAC;QAAC,OAAQ,KAAK,EAAG,CAAC;YAElB,OAAO,IAAI,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,YAAY,SAAS,CAAC,CAAC;QAEjG,CAAC;IAEF,CAAC;IAGD,SAAS,IAAI,CAAC,OAAe,EAAE,KAAc;QAE5C,OAAO,CAAC,KAAK,CAAC,KAAK;YAClB,CAAC,CAAC,GAAG,IAAI,KAAK,OAAO,UAAU,IAAI,gCAAgC;YACnE,CAAC,CAAC,GAAG,IAAI,KAAK,OAAO,EAAE,CACvB,CAAC;QAEF,OAAO,CAAC,CAAC;IAEV,CAAC;AAEF,CAAC"}