@weapp-vite/language-service 0.0.0-stage → 0.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 +61 -2
- package/dist/index-zymq0CcB.d.mts +414 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -0
- package/dist/project/index.d.mts +39 -0
- package/dist/project/index.mjs +365 -0
- package/dist/src-DeWCgmtC.mjs +24326 -0
- package/package.json +84 -3
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { A as resolveLanguageProfile, S as TEMPLATE_EXTENSIONS, d as LanguageError, j as languageProfiles, k as isScriptModuleFile, t as analyzeDocument } from "../src-DeWCgmtC.mjs";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
|
+
import ignore from "ignore";
|
|
6
|
+
// src/project/paths.ts
|
|
7
|
+
/** 由项目内相对路径限定祖先层数,兼容 glob 的正斜杠路径并禁止越过项目根。 */
|
|
8
|
+
function projectAncestorDirectories(root, file, pathApi = path) {
|
|
9
|
+
root = pathApi.resolve(root);
|
|
10
|
+
const relative = pathApi.relative(root, pathApi.dirname(pathApi.resolve(file)));
|
|
11
|
+
if (relative === ".." || relative.startsWith(`..${pathApi.sep}`) || pathApi.isAbsolute(relative)) throw new Error("项目检查路径必须位于配置所属项目内");
|
|
12
|
+
const segments = relative ? relative.split(pathApi.sep) : [];
|
|
13
|
+
const directories = [];
|
|
14
|
+
for (let depth = segments.length; depth >= 0; depth--) directories.push(pathApi.resolve(root, ...segments.slice(0, depth)));
|
|
15
|
+
return directories;
|
|
16
|
+
}
|
|
17
|
+
// src/project/files.ts
|
|
18
|
+
const PROJECT_IGNORES = [
|
|
19
|
+
"**/.git/**",
|
|
20
|
+
"**/node_modules/**",
|
|
21
|
+
"**/dist/**",
|
|
22
|
+
"**/dist-*/**",
|
|
23
|
+
"**/.weapp-vite/**",
|
|
24
|
+
"**/miniprogram_npm/**",
|
|
25
|
+
"**/generated/**",
|
|
26
|
+
"**/pnpm-lock.yaml",
|
|
27
|
+
"**/package-lock.json",
|
|
28
|
+
"**/yarn.lock"
|
|
29
|
+
];
|
|
30
|
+
/** 原生模块及待编译模块都由语言适配层处理,不与普通 JS/TS 重复检查。 */
|
|
31
|
+
function isScriptModule(file) {
|
|
32
|
+
return isScriptModuleFile(file);
|
|
33
|
+
}
|
|
34
|
+
function isNativeDocument(file) {
|
|
35
|
+
return isScriptModule(file) || TEMPLATE_EXTENSIONS.some((extension) => file.toLowerCase().endsWith(extension));
|
|
36
|
+
}
|
|
37
|
+
async function globForInput(input, cwd) {
|
|
38
|
+
const negative = input.startsWith("!");
|
|
39
|
+
const file = negative ? input.slice(1) : input;
|
|
40
|
+
const absolute = path.resolve(cwd, file);
|
|
41
|
+
const relative = path.relative(cwd, absolute);
|
|
42
|
+
if (relative.startsWith(`..${path.sep}`) || relative === ".." || path.isAbsolute(relative)) throw new Error("项目检查路径必须位于配置所属项目内");
|
|
43
|
+
const info = await stat(absolute).catch((error) => {
|
|
44
|
+
if (error.code !== "ENOENT") throw error;
|
|
45
|
+
});
|
|
46
|
+
const pattern = info?.isDirectory() ? `${fg.convertPathToPattern(file).replace(/\/$/, "")}/**/*` : info ? fg.convertPathToPattern(file) : file;
|
|
47
|
+
return `${negative ? "!" : ""}${pattern}`;
|
|
48
|
+
}
|
|
49
|
+
/** 各工具共用忽略文件与路径归一化;不跟随符号链接,也不越过项目根。 */
|
|
50
|
+
async function collectProjectFiles(cwd, inputs = ["."], ignorePatterns = []) {
|
|
51
|
+
cwd = path.resolve(cwd);
|
|
52
|
+
const patterns = await Promise.all(inputs.map((input) => globForInput(input, cwd)));
|
|
53
|
+
const files = (await fg(patterns, {
|
|
54
|
+
cwd,
|
|
55
|
+
absolute: true,
|
|
56
|
+
onlyFiles: true,
|
|
57
|
+
dot: true,
|
|
58
|
+
followSymbolicLinks: false,
|
|
59
|
+
ignore: [...PROJECT_IGNORES, ...ignorePatterns]
|
|
60
|
+
})).map((file) => path.resolve(file));
|
|
61
|
+
const matchers = /* @__PURE__ */ new Map();
|
|
62
|
+
async function excluded(file) {
|
|
63
|
+
for (const directory of projectAncestorDirectories(cwd, file)) {
|
|
64
|
+
let matcher = matchers.get(directory);
|
|
65
|
+
if (!matcher) {
|
|
66
|
+
matcher = ignore();
|
|
67
|
+
for (const name of directory === cwd ? [".gitignore", ".prettierignore"] : [".gitignore"]) {
|
|
68
|
+
const contents = await readFile(path.join(directory, name), "utf8").catch((error) => {
|
|
69
|
+
if (error.code !== "ENOENT") throw error;
|
|
70
|
+
return "";
|
|
71
|
+
});
|
|
72
|
+
matcher.add(contents);
|
|
73
|
+
}
|
|
74
|
+
matchers.set(directory, matcher);
|
|
75
|
+
}
|
|
76
|
+
if (matcher.ignores(path.relative(directory, file).split(path.sep).join("/"))) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const included = [];
|
|
81
|
+
for (const file of files.sort()) if (!await excluded(file)) included.push(file);
|
|
82
|
+
return included;
|
|
83
|
+
}
|
|
84
|
+
// src/project/ownership.ts
|
|
85
|
+
/** 后缀自带的编译阶段优先于模板声明;普通 JS 模块保留标签声明的原生方言。 */
|
|
86
|
+
function moduleInput(fileName, owner) {
|
|
87
|
+
if (isScriptModuleFile(fileName)) return {
|
|
88
|
+
sourceKind: /\.(?:wxs|sjs|ks)\.[jt]s$|\.filter\.ts$/i.test(fileName) ? "compiled" : owner.sourceKind,
|
|
89
|
+
language: /\.ts$/i.test(fileName) ? "typescript" : owner.language ?? "javascript",
|
|
90
|
+
.../\.filter\.[jt]s$/i.test(fileName) ? { dialect: "javascript" } : {}
|
|
91
|
+
};
|
|
92
|
+
return {
|
|
93
|
+
dialect: owner.dialect,
|
|
94
|
+
sourceKind: /\.tsx?$/i.test(fileName) ? "compiled" : owner.sourceKind,
|
|
95
|
+
language: /\.tsx?$/i.test(fileName) ? "typescript" : owner.language ?? "javascript"
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function resolveProjectModule(root, parent, source, available) {
|
|
99
|
+
if (!source.startsWith(".") && !source.startsWith("/") || source.includes("{{")) return;
|
|
100
|
+
const absolute = source.startsWith("/") ? path.resolve(root, `.${source}`) : path.resolve(path.dirname(parent), source);
|
|
101
|
+
return [
|
|
102
|
+
absolute,
|
|
103
|
+
`${absolute}.ts`,
|
|
104
|
+
`${absolute}.js`,
|
|
105
|
+
absolute.replace(/\.js$/i, ".ts"),
|
|
106
|
+
...[
|
|
107
|
+
"ts",
|
|
108
|
+
"js",
|
|
109
|
+
"sjs",
|
|
110
|
+
"wxs",
|
|
111
|
+
"ks"
|
|
112
|
+
].map((extension) => `${absolute}.${extension}`)
|
|
113
|
+
].find((file) => available.has(file));
|
|
114
|
+
}
|
|
115
|
+
/** 引用图按上下文到达稳定点;既支持循环,也支持模板引用普通 filter.js。 */
|
|
116
|
+
async function collectModuleOwners(root, available, documents, load, options) {
|
|
117
|
+
const owners = /* @__PURE__ */ new Map();
|
|
118
|
+
const pending = [];
|
|
119
|
+
const add = (file, owner) => {
|
|
120
|
+
const explicit = resolveLanguageProfile(path.relative(root, file).split(path.sep).join("/"), {
|
|
121
|
+
...options,
|
|
122
|
+
defaultPlatform: void 0
|
|
123
|
+
});
|
|
124
|
+
owner = {
|
|
125
|
+
...owner,
|
|
126
|
+
platform: explicit?.platform ?? owner.platform
|
|
127
|
+
};
|
|
128
|
+
const existing = owners.get(file) ?? [];
|
|
129
|
+
if (!existing.some((candidate) => JSON.stringify(candidate) === JSON.stringify(owner))) {
|
|
130
|
+
owners.set(file, [...existing, owner]);
|
|
131
|
+
pending.push({
|
|
132
|
+
file,
|
|
133
|
+
owner
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
for (const document of documents.values()) {
|
|
138
|
+
const platform = document.analysis.template?.platform;
|
|
139
|
+
if (!platform) continue;
|
|
140
|
+
for (const reference of document.analysis.template?.references ?? []) {
|
|
141
|
+
if (reference.kind !== "script-import" || !reference.source) continue;
|
|
142
|
+
const file = resolveProjectModule(root, document.file, reference.source, available);
|
|
143
|
+
if (file) add(file, {
|
|
144
|
+
platform,
|
|
145
|
+
dialect: reference.tagName === "wxs" ? "wxs" : reference.tagName === "ks" ? "ks" : ["sjs", "import-sjs"].includes(reference.tagName ?? "") ? "sjs" : "javascript",
|
|
146
|
+
sourceKind: reference.sourceKind ?? "native",
|
|
147
|
+
...reference.language ? { language: reference.language } : {}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const document of documents.values()) {
|
|
152
|
+
const script = document.analysis.script;
|
|
153
|
+
if (script && document.platform && !owners.has(document.file)) add(document.file, {
|
|
154
|
+
platform: document.platform,
|
|
155
|
+
dialect: script.dialect,
|
|
156
|
+
sourceKind: script.sourceKind,
|
|
157
|
+
...document.input.language ? { language: document.input.language } : {}
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
for (let index = 0; index < pending.length; index++) {
|
|
161
|
+
const { file, owner } = pending[index];
|
|
162
|
+
const document = await load(file, {
|
|
163
|
+
...moduleInput(file, owner),
|
|
164
|
+
platform: owner.platform
|
|
165
|
+
});
|
|
166
|
+
for (const reference of document.analysis.script?.references ?? []) {
|
|
167
|
+
const target = resolveProjectModule(root, file, reference.source, available);
|
|
168
|
+
if (target) {
|
|
169
|
+
const native = /\.(?:wxs|sjs|ks)$|\.filter\.js$/i.test(target);
|
|
170
|
+
add(target, {
|
|
171
|
+
platform: owner.platform,
|
|
172
|
+
dialect: owner.dialect,
|
|
173
|
+
sourceKind: native ? "native" : "compiled"
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return owners;
|
|
179
|
+
}
|
|
180
|
+
// src/project/analyze.ts
|
|
181
|
+
/** 从模板引用传播模块所属平台,不根据共享的 SJS 后缀猜测宿主。 */
|
|
182
|
+
async function analyzeProject(root, inputs = ["."], options = {}, ignores = [], sourceOverrides = /* @__PURE__ */ new Map()) {
|
|
183
|
+
root = path.resolve(root);
|
|
184
|
+
const available = new Set(await collectProjectFiles(root, ["."], ignores));
|
|
185
|
+
const selected = new Set(await collectProjectFiles(root, inputs, ignores));
|
|
186
|
+
for (const file of sourceOverrides.keys()) {
|
|
187
|
+
const relative = path.relative(root, file);
|
|
188
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error("编辑文档必须位于所属项目内");
|
|
189
|
+
available.add(file);
|
|
190
|
+
selected.add(file);
|
|
191
|
+
}
|
|
192
|
+
const documents = /* @__PURE__ */ new Map();
|
|
193
|
+
async function load(file, context = {}) {
|
|
194
|
+
const existing = documents.get(file);
|
|
195
|
+
if (existing) {
|
|
196
|
+
if (!Object.keys(context).length) return existing;
|
|
197
|
+
const input = {
|
|
198
|
+
...existing.input,
|
|
199
|
+
...context
|
|
200
|
+
};
|
|
201
|
+
const analysis = analyzeDocument(input);
|
|
202
|
+
return {
|
|
203
|
+
...existing,
|
|
204
|
+
input,
|
|
205
|
+
analysis,
|
|
206
|
+
platform: analysis.platform
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
const source = sourceOverrides.get(file) ?? await readFile(file, "utf8");
|
|
210
|
+
const fileName = path.relative(root, file).split(path.sep).join("/");
|
|
211
|
+
const input = {
|
|
212
|
+
source,
|
|
213
|
+
fileName,
|
|
214
|
+
options,
|
|
215
|
+
...context
|
|
216
|
+
};
|
|
217
|
+
const analysis = analyzeDocument(input);
|
|
218
|
+
const document = {
|
|
219
|
+
file,
|
|
220
|
+
fileName,
|
|
221
|
+
source,
|
|
222
|
+
input,
|
|
223
|
+
platform: analysis.platform,
|
|
224
|
+
analysis
|
|
225
|
+
};
|
|
226
|
+
documents.set(file, document);
|
|
227
|
+
return document;
|
|
228
|
+
}
|
|
229
|
+
for (const file of available) if (isNativeDocument(file)) await load(file);
|
|
230
|
+
const owners = await collectModuleOwners(root, available, documents, load, options);
|
|
231
|
+
for (const document of documents.values()) {
|
|
232
|
+
if (TEMPLATE_EXTENSIONS.some((extension) => document.fileName.toLowerCase().endsWith(extension))) continue;
|
|
233
|
+
const inherited = owners.get(document.file);
|
|
234
|
+
const explicit = resolveLanguageProfile(document.fileName, {
|
|
235
|
+
...options,
|
|
236
|
+
defaultPlatform: void 0
|
|
237
|
+
});
|
|
238
|
+
const platforms = new Set(inherited?.map((owner) => owner.platform));
|
|
239
|
+
const contexts = new Set(inherited?.map((owner) => JSON.stringify(moduleInput(document.fileName, owner))));
|
|
240
|
+
if (inherited?.length) {
|
|
241
|
+
const owner = inherited[0];
|
|
242
|
+
document.input = {
|
|
243
|
+
...document.input,
|
|
244
|
+
...moduleInput(document.fileName, owner),
|
|
245
|
+
platform: explicit?.platform ?? (platforms.size === 1 ? owner.platform : void 0)
|
|
246
|
+
};
|
|
247
|
+
document.analysis = analyzeDocument(document.input);
|
|
248
|
+
document.platform = document.analysis.platform;
|
|
249
|
+
}
|
|
250
|
+
if (!explicit && platforms.size > 1 || contexts.size > 1) {
|
|
251
|
+
const diagnostic = {
|
|
252
|
+
code: "language/ambiguous-platform",
|
|
253
|
+
severity: "error",
|
|
254
|
+
start: 0,
|
|
255
|
+
end: 0,
|
|
256
|
+
message: `模块存在冲突的引用归属(${[...platforms].join("、")}),请统一源文件方言或用 weapp.language.overrides 明确平台。`
|
|
257
|
+
};
|
|
258
|
+
document.analysis = {
|
|
259
|
+
...document.analysis,
|
|
260
|
+
valid: false,
|
|
261
|
+
diagnostics: [...document.analysis.diagnostics, diagnostic]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const diagnostics = document.analysis.diagnostics.map((diagnostic) => diagnostic.code === "script/platform-required" ? {
|
|
265
|
+
...diagnostic,
|
|
266
|
+
severity: "error"
|
|
267
|
+
} : diagnostic);
|
|
268
|
+
document.analysis = {
|
|
269
|
+
...document.analysis,
|
|
270
|
+
valid: diagnostics.every((diagnostic) => diagnostic.severity !== "error"),
|
|
271
|
+
diagnostics
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return [...documents.values()].filter((document) => selected.has(document.file)).sort((left, right) => left.fileName.localeCompare(right.fileName));
|
|
275
|
+
}
|
|
276
|
+
/** 编辑器传入未保存源码,沿用与 CLI 相同的项目引用归属和配置。 */
|
|
277
|
+
async function resolveProjectDocumentInput(project, fileName, source) {
|
|
278
|
+
const file = path.resolve(project.root, fileName);
|
|
279
|
+
const document = (await analyzeProject(project.root, [path.relative(project.root, file).split(path.sep).join("/")], project.language, [], /* @__PURE__ */ new Map([[file, source]]))).find((candidate) => candidate.file === file);
|
|
280
|
+
const platformErrors = document?.analysis.diagnostics.filter((diagnostic) => diagnostic.code === "language/ambiguous-platform" || diagnostic.code === "script/platform-required") ?? [];
|
|
281
|
+
if (platformErrors.length) throw new LanguageError(fileName, platformErrors);
|
|
282
|
+
return document?.input ?? {
|
|
283
|
+
source,
|
|
284
|
+
fileName: path.relative(project.root, file).split(path.sep).join("/"),
|
|
285
|
+
options: project.language
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/** 编辑器和项目图共用静态路径解析,包含项目根路径及编译源替代。 */
|
|
289
|
+
async function resolveProjectReference(project, fileName, source) {
|
|
290
|
+
const files = new Set(await collectProjectFiles(project.root));
|
|
291
|
+
return resolveProjectModule(project.root, path.resolve(project.root, fileName), source, files);
|
|
292
|
+
}
|
|
293
|
+
/** 诊断位置以 UTF-16 计算,输出使用稳定的相对路径与一基行列。 */
|
|
294
|
+
function printDiagnostic(fileName, source, diagnostic) {
|
|
295
|
+
const prefix = source.slice(0, diagnostic.start);
|
|
296
|
+
const line = prefix.split("\n").length;
|
|
297
|
+
const column = prefix.length - prefix.lastIndexOf("\n");
|
|
298
|
+
return `${fileName.replaceAll("\\", "/")}:${line}:${column} ${diagnostic.severity} ${diagnostic.code} ${diagnostic.message}`;
|
|
299
|
+
}
|
|
300
|
+
// src/project/config.ts
|
|
301
|
+
function platform(value) {
|
|
302
|
+
return typeof value === "string" && languageProfiles.some((profile) => profile.platform === value);
|
|
303
|
+
}
|
|
304
|
+
function languageOptions(value, fallback) {
|
|
305
|
+
if (value !== void 0 && (!value || typeof value !== "object" || Array.isArray(value))) throw new Error("weapp.language 必须为对象");
|
|
306
|
+
const options = value ?? {};
|
|
307
|
+
if (options.platform !== void 0 && !platform(options.platform)) throw new Error(`无效的 weapp.language.platform:${String(options.platform)}`);
|
|
308
|
+
if (options.defaultPlatform !== void 0 && !platform(options.defaultPlatform)) throw new Error(`无效的 weapp.language.defaultPlatform:${String(options.defaultPlatform)}`);
|
|
309
|
+
if (options.overrides !== void 0 && (!Array.isArray(options.overrides) || options.overrides.some((override) => !override || !Array.isArray(override.files) || override.files.some((file) => typeof file !== "string") || !platform(override.platform)))) throw new Error("weapp.language.overrides 需要 files glob 和已登记的平台");
|
|
310
|
+
return {
|
|
311
|
+
...platform(fallback) ? { defaultPlatform: fallback } : {},
|
|
312
|
+
...options
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
/** 只在可信项目执行配置;工作区检查使用父配置,普通格式化使用最近配置。 */
|
|
316
|
+
async function loadProjectConfig(cwd, options = {}) {
|
|
317
|
+
if (options.configFile?.endsWith(".json")) {
|
|
318
|
+
const file = path.resolve(cwd, options.configFile);
|
|
319
|
+
const value = JSON.parse(await readFile(file, "utf8"));
|
|
320
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("格式配置必须为对象");
|
|
321
|
+
return {
|
|
322
|
+
...await loadViteProjectConfig(cwd, { workspaceFirst: options.workspaceFirst }, false),
|
|
323
|
+
[options.configKind ?? "fmt"]: value
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
return loadViteProjectConfig(cwd, options);
|
|
327
|
+
}
|
|
328
|
+
async function loadViteProjectConfig(cwd, options, includeJson = true) {
|
|
329
|
+
const vite = await import("vite-plus").catch((cause) => {
|
|
330
|
+
throw new Error("读取项目语言配置需要项目安装 Vite+ >=1.2.0。", { cause });
|
|
331
|
+
});
|
|
332
|
+
const workspace = vite.searchForWorkspaceRoot(cwd);
|
|
333
|
+
const directories = options.workspaceFirst ? [workspace] : [];
|
|
334
|
+
for (let current = cwd; !directories.includes(current); current = path.dirname(current)) {
|
|
335
|
+
directories.push(current);
|
|
336
|
+
if (current === workspace || current === path.dirname(current)) break;
|
|
337
|
+
}
|
|
338
|
+
for (const directory of directories) {
|
|
339
|
+
const loaded = await vite.loadConfigFromFile({
|
|
340
|
+
command: "build",
|
|
341
|
+
mode: "production"
|
|
342
|
+
}, options.configFile ? path.resolve(cwd, options.configFile) : void 0, directory, "silent");
|
|
343
|
+
if (loaded) {
|
|
344
|
+
const config = loaded.config;
|
|
345
|
+
if (config.fmt || config.lint || config.weapp) return {
|
|
346
|
+
root: path.dirname(loaded.path),
|
|
347
|
+
fmt: config.fmt,
|
|
348
|
+
lint: config.lint,
|
|
349
|
+
language: languageOptions(config.weapp?.language, config.weapp?.platform)
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (options.configFile) throw new Error(`${options.configFile} 没有导出项目检查配置`);
|
|
353
|
+
try {
|
|
354
|
+
if (!includeJson) continue;
|
|
355
|
+
return await loadProjectConfig(directory, { configFile: ".oxfmtrc.json" });
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (error.code !== "ENOENT") throw error;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
root: cwd,
|
|
362
|
+
language: {}
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
export { PROJECT_IGNORES, analyzeProject, collectProjectFiles, isNativeDocument, isScriptModule, loadProjectConfig, printDiagnostic, resolveProjectDocumentInput, resolveProjectReference };
|