@mizen-labs/cli 0.0.0-stage → 0.1.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.
@@ -0,0 +1,94 @@
1
+ import { isAbsolute, relative } from "node:path";
2
+ import { plural, row, status as statusLine } from "../output.js";
3
+ export function check(id, status, message, evidence = [], repair) {
4
+ return {
5
+ evidence,
6
+ id,
7
+ message,
8
+ status,
9
+ ...(repair && status !== "pass" ? { repair } : {}),
10
+ };
11
+ }
12
+ export function report(project, diagnostics) {
13
+ const errors = diagnostics.filter((item) => item.status === "error").length;
14
+ const unverified = diagnostics.filter((item) => item.status === "unverified").length;
15
+ let status = "healthy";
16
+ if (unverified) {
17
+ status = "incomplete";
18
+ }
19
+ if (errors) {
20
+ status = "failed";
21
+ }
22
+ return {
23
+ diagnostics,
24
+ project,
25
+ status,
26
+ summary: {
27
+ errors,
28
+ passed: diagnostics.length - errors - unverified,
29
+ unverified,
30
+ },
31
+ };
32
+ }
33
+ // Paths inside the app read relative to it; others stay absolute.
34
+ function shown(path, base) {
35
+ if (!base) {
36
+ return path;
37
+ }
38
+ const inside = relative(base, path);
39
+ return inside && !inside.startsWith("..") && !isAbsolute(inside)
40
+ ? inside
41
+ : path;
42
+ }
43
+ /** A finding as a status headline with its check, evidence, and fix. */
44
+ export function formatDiagnostic(style, item, base = null) {
45
+ return [
46
+ statusLine(style, item.status === "error" ? "fail" : "warn", item.message),
47
+ row("Check", item.id),
48
+ ...(item.evidence.length
49
+ ? [
50
+ row("Evidence", item.evidence.map((path) => shown(path, base))),
51
+ ]
52
+ : []),
53
+ ...(item.repair ? [row("Next step", item.repair)] : []),
54
+ "",
55
+ ];
56
+ }
57
+ /** The closing line: the outcome, then the counts. */
58
+ export function summaryLine(style, result) {
59
+ const { errors, passed, unverified } = result.summary;
60
+ if (errors) {
61
+ return statusLine(style, "fail", `Found ${plural(errors, "error")}. ${unverified} unverified, ${passed} passed.`);
62
+ }
63
+ if (unverified) {
64
+ return statusLine(style, "warn", `Couldn't verify ${plural(unverified, "check")}. ${passed} passed.`);
65
+ }
66
+ return statusLine(style, "pass", `Healthy. ${plural(passed, "check")} passed.`);
67
+ }
68
+ // Each passing check's id beside its message, in one aligned column.
69
+ function passedRows(passed) {
70
+ const width = Math.max(...passed.map((item) => item.id.length)) + 2;
71
+ return passed.map((item) => ` ${item.id.padEnd(width)}${item.message}`);
72
+ }
73
+ export function formatReport(style, result) {
74
+ const findings = result.diagnostics.filter((item) => item.status !== "pass");
75
+ const passed = result.diagnostics.filter((item) => item.status === "pass");
76
+ return [
77
+ row("Project", result.project ?? "Not found"),
78
+ "",
79
+ ...findings.flatMap((item) => formatDiagnostic(style, item, result.project)),
80
+ ...(passed.length ? [style.bold("Passed"), ...passedRows(passed), ""] : []),
81
+ summaryLine(style, result),
82
+ ].join("\n");
83
+ }
84
+ /** A diagnostic as an error block's rows, and in full for machines. */
85
+ export function diagnosticDetail(item) {
86
+ const rows = [
87
+ ["Check", item.id],
88
+ ...(item.evidence.length
89
+ ? [["Evidence", item.evidence.join("\n")]]
90
+ : []),
91
+ ...(item.repair ? [["Next step", item.repair]] : []),
92
+ ];
93
+ return { data: { diagnostic: item }, rows };
94
+ }
@@ -0,0 +1,107 @@
1
+ import { join } from "node:path";
2
+ import { viteSetupChecks } from "../vite/doctor.js";
3
+ import { readOptional } from "./files.js";
4
+ import { check } from "./report.js";
5
+ import { documentFunction, imports, isNode, jsxName, literal, literalTrue, nameOf, nodes, parseSource, property, staticArrayContains, staticConfig, } from "./syntax.js";
6
+ async function sourceAt(project, candidates) {
7
+ const found = (await Promise.all(candidates.map(async (candidate) => {
8
+ const path = join(project, candidate);
9
+ const source = await readOptional(path);
10
+ return source === null ? null : { path, source };
11
+ }))).filter((item) => item !== null);
12
+ return found.length === 1 ? found[0] : null;
13
+ }
14
+ async function configCheck(project, framework) {
15
+ const prefix = framework === "next" ? "next.config" : "vite.config";
16
+ const file = await sourceAt(project, ["js", "mjs", "cjs", "ts", "mts", "cts"].map((extension) => `${prefix}.${extension}`));
17
+ const repair = framework === "next"
18
+ ? 'Include "@mizen-labs/ui" in transpilePackages.'
19
+ : 'Include "@mizen-labs/ui" in ssr.noExternal.';
20
+ if (!file) {
21
+ return check("setup.bundling", "unverified", "No single supported framework configuration was found.", [], repair);
22
+ }
23
+ let ast;
24
+ try {
25
+ ast = parseSource(file.source);
26
+ }
27
+ catch {
28
+ return check("setup.bundling", "unverified", "The configuration could not be parsed; it was not executed.", [file.path], "Check its syntax and verify the Mizen bundling setting manually.");
29
+ }
30
+ const config = staticConfig(ast);
31
+ const value = framework === "next"
32
+ ? property(config, "transpilePackages")
33
+ : property(property(config, "ssr"), "noExternal");
34
+ const present = framework === "start" && literalTrue(value)
35
+ ? true
36
+ : staticArrayContains(value, "@mizen-labs/ui");
37
+ return check("setup.bundling", present === true ? "pass" : "unverified", present === true
38
+ ? "The static configuration includes Mizen in framework bundling."
39
+ : "Bundling could not be verified. Computed, wrapped, or absent settings require manual review.", [file.path], repair);
40
+ }
41
+ function aliases(ast, exported) {
42
+ return imports(ast)
43
+ .filter((node) => ["@mizen-labs/ui/theme", "@mizen-labs/ui"].includes(literal(node.source) ?? ""))
44
+ .flatMap((node) => (Array.isArray(node.specifiers) ? node.specifiers : []))
45
+ .filter((node) => isNode(node) &&
46
+ node.importKind !== "type" &&
47
+ nameOf(node.imported) === exported)
48
+ .map((node) => (isNode(node) ? nameOf(node.local) : null))
49
+ .filter((name) => name !== null);
50
+ }
51
+ function themeCheck(ast, path, document) {
52
+ const scripts = aliases(ast, "ThemeScript");
53
+ const providers = aliases(ast, "ThemeProvider");
54
+ const heads = nodes(document).filter((node) => jsxName(node) === "head");
55
+ const bodies = nodes(document).filter((node) => jsxName(node) === "body");
56
+ const scriptInHead = heads.some((head) => nodes(head).some((node) => scripts.includes(jsxName(node) ?? "")));
57
+ const providerInBody = bodies.some((body) => nodes(body).some((node) => providers.includes(jsxName(node) ?? "")));
58
+ // Presence is evidence of syntax, not proof of runtime rendering or first paint.
59
+ return check("setup.theme", scriptInHead && providerInBody ? "pass" : "unverified", scriptInHead && providerInBody
60
+ ? "Root syntax contains ThemeScript in head and ThemeProvider in body; runtime behavior still needs a browser check."
61
+ : "Root theme bootstrap is not statically verified. It may be delegated to another component.", [path], "Render ThemeScript before body content and ThemeProvider around the app. Verify first paint and hydration in a browser.");
62
+ }
63
+ function stylesheetCheck(ast, path) {
64
+ const cssImports = imports(ast).filter((node) => literal(node.source) === "@mizen-labs/ui/styles.css");
65
+ const direct = cssImports.some((node) => Array.isArray(node.specifiers) && node.specifiers.length === 0);
66
+ const urlBindings = imports(ast)
67
+ .filter((node) => literal(node.source) === "@mizen-labs/ui/styles.css?url")
68
+ .flatMap((node) => (Array.isArray(node.specifiers) ? node.specifiers : []))
69
+ .map((node) => (isNode(node) ? nameOf(node.local) : null))
70
+ .filter((name) => name !== null);
71
+ const link = nodes(ast).some((node) => urlBindings.includes(nameOf(property(node, "href")) ?? "") &&
72
+ literal(property(node, "rel")) === "stylesheet");
73
+ const present = direct || link;
74
+ return check("setup.stylesheet", present ? "pass" : "unverified", present
75
+ ? "Root syntax declares Mizen's compiled stylesheet. Verify delivery in a browser."
76
+ : "No direct stylesheet import or URL/link declaration was verified. Indirect CSS or custom extraction may supply it.", [path], "Import @mizen-labs/ui/styles.css at the app root, or verify your equivalent CSS pipeline manually.");
77
+ }
78
+ export async function setupChecks(project, framework) {
79
+ if (framework === "vite") {
80
+ return viteSetupChecks(project);
81
+ }
82
+ const candidates = framework === "next"
83
+ ? ["app/layout", "src/app/layout"]
84
+ : ["src/routes/__root", "app/routes/__root"];
85
+ const root = await sourceAt(project, candidates.flatMap((path) => ["tsx", "jsx", "ts", "js"].map((extension) => `${path}.${extension}`)));
86
+ const config = await configCheck(project, framework);
87
+ if (!root) {
88
+ return [
89
+ config,
90
+ check("setup.root", "unverified", "No single conventional root layout was found.", [], "Use Next App Router or review the custom Start root configuration manually."),
91
+ ];
92
+ }
93
+ try {
94
+ const ast = parseSource(root.source);
95
+ return [
96
+ config,
97
+ stylesheetCheck(ast, root.path),
98
+ themeCheck(ast, root.path, documentFunction(ast, framework)),
99
+ ];
100
+ }
101
+ catch {
102
+ return [
103
+ config,
104
+ check("setup.root", "unverified", "The root module could not be parsed; it was not executed.", [root.path], "Check root module syntax and verify styles and theme setup manually."),
105
+ ];
106
+ }
107
+ }
@@ -0,0 +1,148 @@
1
+ import { parse } from "@babel/parser";
2
+ export function isNode(value) {
3
+ return (!!value &&
4
+ typeof value === "object" &&
5
+ "type" in value &&
6
+ typeof value.type === "string");
7
+ }
8
+ export function nodes(value) {
9
+ if (Array.isArray(value)) {
10
+ return value.flatMap(nodes);
11
+ }
12
+ if (!isNode(value)) {
13
+ return [];
14
+ }
15
+ return [value, ...Object.values(value).flatMap(nodes)];
16
+ }
17
+ export function parseSource(source) {
18
+ return parse(source, {
19
+ attachComment: false,
20
+ plugins: ["jsx", "typescript"],
21
+ sourceType: "unambiguous",
22
+ });
23
+ }
24
+ export function literal(node) {
25
+ return isNode(node) &&
26
+ node.type === "StringLiteral" &&
27
+ typeof node.value === "string"
28
+ ? node.value
29
+ : null;
30
+ }
31
+ export function nameOf(node) {
32
+ if (!isNode(node)) {
33
+ return null;
34
+ }
35
+ if (typeof node.name === "string") {
36
+ return node.name;
37
+ }
38
+ return literal(node);
39
+ }
40
+ function unwrap(node) {
41
+ if (!isNode(node)) {
42
+ return node;
43
+ }
44
+ if (["TSAsExpression", "TSSatisfiesExpression", "TSNonNullExpression"].includes(node.type)) {
45
+ return unwrap(node.expression);
46
+ }
47
+ return node;
48
+ }
49
+ export function staticConfig(ast) {
50
+ const exported = nodes(ast).find((node) => node.type === "ExportDefaultDeclaration");
51
+ if (!exported) {
52
+ return null;
53
+ }
54
+ let value = unwrap(exported.declaration);
55
+ if (isNode(value) && value.type === "CallExpression") {
56
+ value = viteConfigArgument(ast, value);
57
+ }
58
+ if (isNode(value) && value.type === "Identifier") {
59
+ const identifier = value.name;
60
+ if (nodes(ast).filter((node) => node.type === "Identifier" && node.name === identifier).length !== 2) {
61
+ return null;
62
+ }
63
+ const variable = nodes(ast).find((node) => node.type === "VariableDeclarator" && nameOf(node.id) === identifier);
64
+ value = unwrap(variable?.init);
65
+ }
66
+ return isNode(value) && value.type === "ObjectExpression" ? value : null;
67
+ }
68
+ export function property(object, key) {
69
+ if (!isNode(object) ||
70
+ object.type !== "ObjectExpression" ||
71
+ !Array.isArray(object.properties)) {
72
+ return null;
73
+ }
74
+ // Any spread or computed key can override a literal property at runtime.
75
+ if (object.properties.some((item) => !(isNode(item) && ["ObjectProperty", "ObjectMethod"].includes(item.type)) || item.computed)) {
76
+ return null;
77
+ }
78
+ const found = object.properties.filter((item) => isNode(item) && nameOf(item.key) === key);
79
+ if (found.length !== 1) {
80
+ return null;
81
+ }
82
+ const selected = found[0];
83
+ return selected.type === "ObjectProperty" ? selected.value : null;
84
+ }
85
+ export function staticArrayContains(value, expected) {
86
+ if (!isNode(value) ||
87
+ value.type !== "ArrayExpression" ||
88
+ !Array.isArray(value.elements)) {
89
+ return null;
90
+ }
91
+ const strings = value.elements.map(literal);
92
+ if (strings.some((item) => item === null)) {
93
+ return null;
94
+ }
95
+ return strings.includes(expected);
96
+ }
97
+ export function imports(ast) {
98
+ return nodes(ast).filter((node) => node.type === "ImportDeclaration" && node.importKind !== "type");
99
+ }
100
+ export function jsxName(node) {
101
+ return isNode(node) &&
102
+ node.type === "JSXElement" &&
103
+ isNode(node.openingElement)
104
+ ? nameOf(node.openingElement.name)
105
+ : null;
106
+ }
107
+ function viteConfigArgument(ast, call) {
108
+ const allowed = imports(ast)
109
+ .filter((node) => literal(node.source) === "vite")
110
+ .flatMap((node) => (Array.isArray(node.specifiers) ? node.specifiers : []))
111
+ .filter((node) => isNode(node) && nameOf(node.imported) === "defineConfig")
112
+ .map((node) => (isNode(node) ? nameOf(node.local) : null));
113
+ if (!(allowed.includes(nameOf(call.callee)) && Array.isArray(call.arguments)) ||
114
+ call.arguments.length !== 1) {
115
+ return null;
116
+ }
117
+ return unwrap(call.arguments[0]);
118
+ }
119
+ function topLevelFunction(ast, name) {
120
+ if (!(isNode(ast.program) && Array.isArray(ast.program.body))) {
121
+ return null;
122
+ }
123
+ return (ast.program.body.find((node) => isNode(node) &&
124
+ node.type === "FunctionDeclaration" &&
125
+ nameOf(node.id) === name) ?? null);
126
+ }
127
+ export function documentFunction(ast, framework) {
128
+ if (framework === "next") {
129
+ const exported = nodes(ast).find((node) => node.type === "ExportDefaultDeclaration");
130
+ const value = exported?.declaration;
131
+ if (isNode(value) && value.type === "FunctionDeclaration") {
132
+ return value;
133
+ }
134
+ return topLevelFunction(ast, nameOf(value));
135
+ }
136
+ const routeImports = imports(ast)
137
+ .filter((node) => literal(node.source) === "@tanstack/react-router")
138
+ .flatMap((node) => (Array.isArray(node.specifiers) ? node.specifiers : []))
139
+ .filter((node) => isNode(node) && nameOf(node.imported) === "createRootRoute")
140
+ .map((node) => (isNode(node) ? nameOf(node.local) : null));
141
+ const route = nodes(ast).find((node) => node.type === "CallExpression" &&
142
+ routeImports.includes(nameOf(node.callee)));
143
+ const config = route && Array.isArray(route.arguments) ? route.arguments[0] : null;
144
+ return topLevelFunction(ast, nameOf(property(config, "shellComponent")));
145
+ }
146
+ export function literalTrue(value) {
147
+ return (isNode(value) && value.type === "BooleanLiteral" && value.value === true);
148
+ }
@@ -0,0 +1,210 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, posix, relative, resolve, sep, } from "node:path";
3
+ import glob from "fast-glob";
4
+ import { minimatch } from "minimatch";
5
+ import { parseDocument } from "yaml";
6
+ import { exists, manifestAt, readOptional } from "./files.js";
7
+ import { check } from "./report.js";
8
+ export class WorkspaceConfigError extends Error {
9
+ diagnostic;
10
+ constructor(diagnostic) {
11
+ super(diagnostic.message);
12
+ this.diagnostic = diagnostic;
13
+ }
14
+ }
15
+ function invalid(path, message) {
16
+ throw new WorkspaceConfigError(check("workspace.config", "error", message, [path], "Repair the workspace declaration, then rerun mizen doctor --workspace."));
17
+ }
18
+ function targetError(path, message) {
19
+ throw new WorkspaceConfigError(check("workspace.target", "error", message, [path], "Use --cwd with a declared workspace root, or run mizen doctor --cwd <APP_DIR> without --workspace."));
20
+ }
21
+ function patterns(value, path) {
22
+ if (!(Array.isArray(value) &&
23
+ value.every((item) => typeof item === "string" && item.trim().length > 0))) {
24
+ return invalid(path, "Workspace packages must be an array of nonempty path patterns.");
25
+ }
26
+ return value;
27
+ }
28
+ function npmPatterns(value, path) {
29
+ if (value && typeof value === "object" && "packages" in value) {
30
+ return patterns(value.packages, path);
31
+ }
32
+ return patterns(value, path);
33
+ }
34
+ async function yamlPatterns(path) {
35
+ const source = await readOptional(path);
36
+ if (source === null) {
37
+ return invalid(path, "The workspace declaration disappeared during discovery.");
38
+ }
39
+ const document = parseDocument(source);
40
+ if (document.errors.length || document.warnings.length) {
41
+ return invalid(path, "The pnpm workspace YAML is invalid or contains unsupported tags.");
42
+ }
43
+ const value = yamlValue(document, path);
44
+ if (value === null) {
45
+ return null;
46
+ }
47
+ if (typeof value !== "object" || Array.isArray(value)) {
48
+ return invalid(path, "The pnpm workspace declaration must be a mapping.");
49
+ }
50
+ return "packages" in value ? patterns(value.packages, path) : null;
51
+ }
52
+ export async function hasPnpmWorkspacePackages(project) {
53
+ const path = join(project, "pnpm-workspace.yaml");
54
+ return (await exists(path)) && (await yamlPatterns(path)) !== null;
55
+ }
56
+ function yamlValue(document, path) {
57
+ try {
58
+ return document.toJS();
59
+ }
60
+ catch {
61
+ return invalid(path, "The workspace YAML aliases could not be resolved safely.");
62
+ }
63
+ }
64
+ async function declaration(project) {
65
+ const yaml = join(project, "pnpm-workspace.yaml");
66
+ if (await exists(yaml)) {
67
+ return {
68
+ kind: "pnpm",
69
+ patterns: (await yamlPatterns(yaml)) ?? [],
70
+ project,
71
+ source: yaml,
72
+ };
73
+ }
74
+ try {
75
+ const manifest = await manifestAt(join(project, "package.json"));
76
+ if (manifest?.workspaces !== undefined) {
77
+ return {
78
+ kind: "npm",
79
+ patterns: npmPatterns(manifest.workspaces, join(project, "package.json")),
80
+ project,
81
+ source: join(project, "package.json"),
82
+ };
83
+ }
84
+ }
85
+ catch (error) {
86
+ if (error instanceof WorkspaceConfigError) {
87
+ throw error;
88
+ }
89
+ // A malformed member must not hide its enclosing workspace.
90
+ }
91
+ return null;
92
+ }
93
+ export async function findWorkspace(cwd) {
94
+ let directory;
95
+ try {
96
+ directory = await realpath(resolve(cwd));
97
+ if (!(await lstat(directory)).isDirectory()) {
98
+ return targetError(directory, "Select an existing workspace directory with --cwd.");
99
+ }
100
+ }
101
+ catch {
102
+ return targetError(resolve(cwd), "The requested workspace directory is unavailable.");
103
+ }
104
+ return findDeclaration(directory, resolve(cwd));
105
+ }
106
+ async function findDeclaration(directory, requested) {
107
+ const workspace = await declaration(directory);
108
+ if (workspace) {
109
+ return workspace;
110
+ }
111
+ const parent = dirname(directory);
112
+ return parent === directory
113
+ ? targetError(requested, "No pnpm-workspace.yaml or package.json workspaces declaration was found.")
114
+ : findDeclaration(parent, requested);
115
+ }
116
+ const NEGATION = /^!+/u;
117
+ const LEADING_SLASH = /^\.?\/+/u;
118
+ const DRIVE = /^[a-z]:\//iu;
119
+ const PARENT_SEGMENT = /(?:^|[/({|,])\.\.(?=\/|[)}|,]|$)/u;
120
+ const GROUPED_ABSOLUTE = /[({|,](?:\/|[a-z]:\/)/iu;
121
+ function normalize(pattern, kind) {
122
+ const prefix = pattern.match(NEGATION)?.[0] ?? "";
123
+ let value = pattern.slice(prefix.length).replaceAll("\\", "/");
124
+ if (kind === "npm") {
125
+ value = value.replace(LEADING_SLASH, "");
126
+ }
127
+ return { negative: prefix.length % 2 === 1, value: posix.normalize(value) };
128
+ }
129
+ function outside(value) {
130
+ return (isAbsolute(value) ||
131
+ DRIVE.test(value) ||
132
+ PARENT_SEGMENT.test(value) ||
133
+ GROUPED_ABSOLUTE.test(value) ||
134
+ value === ".." ||
135
+ value.startsWith("../"));
136
+ }
137
+ async function declarationWarnings(workspace) {
138
+ if (workspace.kind !== "pnpm") {
139
+ return [];
140
+ }
141
+ const path = join(workspace.project, "package.json");
142
+ try {
143
+ const manifest = await manifestAt(path);
144
+ if (manifest?.workspaces === undefined) {
145
+ return [];
146
+ }
147
+ const npm = npmPatterns(manifest.workspaces, path).map((pattern) => normalize(pattern, "npm"));
148
+ const pnpm = workspace.patterns.map((pattern) => normalize(pattern, "pnpm"));
149
+ if (JSON.stringify(npm) === JSON.stringify(pnpm)) {
150
+ return [];
151
+ }
152
+ }
153
+ catch {
154
+ // Member inspection separately reports an invalid root manifest.
155
+ return [
156
+ check("workspace.declarations", "unverified", "The package.json workspace declaration cannot be compared with pnpm's declaration.", [path, workspace.source], "Align the declarations. pnpm-workspace.yaml controls this discovery."),
157
+ ];
158
+ }
159
+ return [
160
+ check("workspace.declarations", "unverified", "Workspace declarations differ; pnpm-workspace.yaml controls discovery.", [path, workspace.source], "Align package.json workspaces with the pnpm packages declaration."),
161
+ ];
162
+ }
163
+ export async function workspaceMembers(workspace) {
164
+ const include = [];
165
+ let exclude = [];
166
+ const diagnostics = await declarationWarnings(workspace);
167
+ for (const pattern of workspace.patterns) {
168
+ const { negative, value } = normalize(pattern, workspace.kind);
169
+ if (outside(value) || value.includes("${")) {
170
+ diagnostics.push(check("workspace.pattern", "unverified", `Discovery cannot verify this external or environment-dependent pattern: ${pattern}`, [workspace.source], "Inspect the affected app explicitly with mizen doctor --cwd <APP_DIR>."));
171
+ continue;
172
+ }
173
+ if (negative) {
174
+ exclude.push(value);
175
+ }
176
+ else {
177
+ // npm lets a later positive pattern override a matching earlier exclusion.
178
+ if (workspace.kind === "npm") {
179
+ exclude = exclude.filter((ignored) => !minimatch(value, ignored));
180
+ }
181
+ include.push(value);
182
+ }
183
+ }
184
+ const entries = include.length
185
+ ? await glob(include, {
186
+ cwd: workspace.project,
187
+ followSymbolicLinks: false,
188
+ ignore: ["**/node_modules/**", "**/.git/**", ...exclude],
189
+ objectMode: true,
190
+ onlyFiles: false,
191
+ unique: true,
192
+ })
193
+ : [];
194
+ const directories = entries
195
+ .filter((entry) => entry.dirent.isDirectory() || entry.dirent.isSymbolicLink())
196
+ .map((entry) => resolve(workspace.project, entry.path));
197
+ return {
198
+ diagnostics,
199
+ projects: [...new Set([workspace.project, ...directories])].sort((a, b) => a.localeCompare(b, "en")),
200
+ };
201
+ }
202
+ export async function linkedProject(project) {
203
+ const actual = await realpath(project);
204
+ return process.platform === "win32"
205
+ ? actual.toLowerCase() !== project.toLowerCase()
206
+ : actual !== project;
207
+ }
208
+ export function memberPath(root, project) {
209
+ return relative(root, project).split(sep).join("/") || ".";
210
+ }