@quasar/mcp 1.0.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/src/project.js ADDED
@@ -0,0 +1,189 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
2
+ import { dirname, join, resolve } from 'node:path'
3
+
4
+ /**
5
+ * The packages that ship a docs slice in `dist/mcp`, in the order the
6
+ * server lists them. The slices are generated by the docs site build
7
+ * (`docs/build/mcp`, `--target`) as the last step of each package's
8
+ * `prepublishOnly`.
9
+ */
10
+ export const DOCS_PACKAGES = ['quasar', '@quasar/app-vite']
11
+
12
+ /**
13
+ * The first releases that bundle their docs slice; older installs
14
+ * serve nothing but the ui API JSON, which every quasar release ships.
15
+ */
16
+ export const BUNDLED_DOCS_SINCE = {
17
+ quasar: '2.33.0',
18
+ '@quasar/app-vite': '3.9.0'
19
+ }
20
+
21
+ /**
22
+ * @typedef {object} InstalledPackage
23
+ * @property {string} name
24
+ * @property {string} version
25
+ * @property {string} dir The package root inside the project's node_modules.
26
+ * @property {string | null} docsDir `dist/mcp` when this release bundles its docs.
27
+ * @property {string | null} apiDir `dist/api` for quasar (the component/plugin/directive JSON descriptors).
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} Project
32
+ * @property {string} dir The directory served: the start directory, or the app found below it.
33
+ * @property {string} startDir The directory the server was started in (or given with --project).
34
+ * @property {string[]} otherApps Other app directories found below `startDir`, not served.
35
+ * @property {InstalledPackage[]} packages The DOCS_PACKAGES the project has installed.
36
+ */
37
+
38
+ /** How deep below the start directory the apps of a workspace are looked for. */
39
+ const SCAN_DEPTH = 4
40
+ const SKIP_DIRS = new Set(['node_modules', 'dist', '.quasar', 'coverage'])
41
+
42
+ /**
43
+ * The directories below `root` (itself excluded) that have a docs
44
+ * package installed, in path order, a few levels deep: the apps of a
45
+ * pnpm workspace, whose dependencies are not hoisted to the root.
46
+ * Hidden directories, build output and node_modules are not entered;
47
+ * neither is an app, once found.
48
+ *
49
+ * @param {string} root
50
+ * @returns {string[]}
51
+ */
52
+ function findApps(root) {
53
+ const found = []
54
+ const walk = (dir, depth) => {
55
+ let entries
56
+ try {
57
+ entries = readdirSync(dir, { withFileTypes: true })
58
+ } catch {
59
+ return
60
+ }
61
+ entries.sort((a, b) => a.name.localeCompare(b.name))
62
+ for (const entry of entries) {
63
+ if (
64
+ !entry.isDirectory() ||
65
+ entry.name.startsWith('.') ||
66
+ SKIP_DIRS.has(entry.name)
67
+ ) {
68
+ continue
69
+ }
70
+ const child = join(dir, entry.name)
71
+ const isApp = DOCS_PACKAGES.some(name =>
72
+ existsSync(join(child, 'node_modules', name, 'package.json'))
73
+ )
74
+ if (isApp) {
75
+ found.push(child)
76
+ } else if (depth < SCAN_DEPTH) {
77
+ walk(child, depth + 1)
78
+ }
79
+ }
80
+ }
81
+ walk(root, 1)
82
+ return found
83
+ }
84
+
85
+ /**
86
+ * Whether a directory is worth looking below: a workspace root has a
87
+ * manifest, a home directory or a random folder (where a user-scoped
88
+ * client starts the server) has none, and walking those would be slow
89
+ * and could pick up an unrelated app.
90
+ *
91
+ * @param {string} dir
92
+ * @returns {boolean}
93
+ */
94
+ function isWorkspaceRoot(dir) {
95
+ return ['package.json', 'pnpm-workspace.yaml'].some(file =>
96
+ existsSync(join(dir, file))
97
+ )
98
+ }
99
+
100
+ /**
101
+ * @param {string} dir
102
+ * @returns {InstalledPackage[]}
103
+ */
104
+ function locateAll(dir) {
105
+ const packages = []
106
+ for (const name of DOCS_PACKAGES) {
107
+ const installed = locatePackage(dir, name)
108
+ if (installed !== null) {
109
+ packages.push(installed)
110
+ }
111
+ }
112
+ return packages
113
+ }
114
+
115
+ /**
116
+ * The package as the project's own code would resolve it: the nearest
117
+ * `node_modules/<name>` walking up from the project, so a pnpm strict
118
+ * layout, a hoisted one and a workspace link all land on the copy the
119
+ * project runs. Symlinks are kept as found: the files read the same
120
+ * through them.
121
+ *
122
+ * @param {string} projectDir
123
+ * @param {string} name
124
+ * @returns {InstalledPackage | null}
125
+ */
126
+ function locatePackage(projectDir, name) {
127
+ let dir = null
128
+ for (
129
+ let current = projectDir, parent = dirname(current);
130
+ dir === null;
131
+ current = parent, parent = dirname(current)
132
+ ) {
133
+ const candidate = join(current, 'node_modules', name)
134
+ if (existsSync(join(candidate, 'package.json'))) {
135
+ dir = candidate
136
+ } else if (parent === current) {
137
+ return null
138
+ }
139
+ }
140
+
141
+ const packageJsonPath = join(dir, 'package.json')
142
+ const { version } = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
143
+ const docsDir = join(dir, 'dist', 'mcp')
144
+ const apiDir = join(dir, 'dist', 'api')
145
+
146
+ return {
147
+ name,
148
+ version,
149
+ dir,
150
+ docsDir: existsSync(join(docsDir, 'meta.json')) ? docsDir : null,
151
+ apiDir: name === 'quasar' && existsSync(apiDir) ? apiDir : null
152
+ }
153
+ }
154
+
155
+ /**
156
+ * The project to serve. The packages resolve from `projectDir` the way
157
+ * its own code resolves them. When that finds nothing and the directory
158
+ * was not named explicitly and it has a package.json or a
159
+ * pnpm-workspace.yaml, the apps below it are looked for (a
160
+ * workspace opened at its root): the first one, a full app before a
161
+ * library and in path order otherwise, is served and the others are
162
+ * reported, for --project to pick.
163
+ *
164
+ * @param {string} [projectDir] Defaults to the current working directory.
165
+ * @param {{ explicit?: boolean }} [opts] `explicit`: the directory was given (--project), serve it as is.
166
+ * @returns {Project}
167
+ */
168
+ export function loadProject(
169
+ projectDir = process.cwd(),
170
+ { explicit = false } = {}
171
+ ) {
172
+ const startDir = resolve(projectDir)
173
+ let dir = startDir
174
+ let packages = locateAll(startDir)
175
+ let otherApps = []
176
+ if (packages.length === 0 && !explicit && isWorkspaceRoot(startDir)) {
177
+ // a full app (ui and CLI) before a package that only depends on
178
+ // quasar, a component library in the workspace; path order otherwise
179
+ const apps = findApps(startDir)
180
+ .map(app => ({ app, packages: locateAll(app) }))
181
+ .sort((a, b) => b.packages.length - a.packages.length)
182
+ if (apps.length !== 0) {
183
+ dir = apps[0].app
184
+ packages = apps[0].packages
185
+ otherApps = apps.slice(1).map(({ app }) => app)
186
+ }
187
+ }
188
+ return { dir, startDir, otherApps, packages }
189
+ }