@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/LICENSE +21 -0
- package/README.md +67 -0
- package/package.json +61 -0
- package/src/api.js +272 -0
- package/src/bin.js +63 -0
- package/src/docs.js +606 -0
- package/src/index.js +2 -0
- package/src/project.js +189 -0
- package/src/server.js +526 -0
- package/src/slugify.js +21 -0
- package/src/updates.js +41 -0
- package/src/version.js +6 -0
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
|
+
}
|