@effect/language-service 0.41.1 → 0.43.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 +53 -1
- package/effect-lsp-patch-utils.js +144 -3
- package/effect-lsp-patch-utils.js.map +1 -1
- package/index.js +435 -278
- package/index.js.map +1 -1
- package/package.json +1 -1
- package/transform.js +144 -3
- package/transform.js.map +1 -1
package/README.md
CHANGED
|
@@ -111,6 +111,7 @@ Few options can be provided alongside the initialization of the Language Service
|
|
|
111
111
|
},
|
|
112
112
|
"quickinfo": true, // controls Effect quickinfo (default: true)
|
|
113
113
|
"quickinfoEffectParameters": "whenTruncated", // (default: "whenTruncated") controls when to display effect type parameters always,never,whenTruncated
|
|
114
|
+
"quickinfoMaximumLength": -1, // controls how long can be the types in the quickinfo hover (helps with very long type to improve perfs, defaults to -1 for no truncation, can be any number eg. 1000 and TS will try to fit as much as possible in that budget, higher number means more info.)
|
|
114
115
|
"completions": true, // controls Effect completions (default: true)
|
|
115
116
|
"goto": true, // controls Effect goto references (default: true)
|
|
116
117
|
"inlays": true, // controls Effect provided inlayHints (default: true)
|
|
@@ -119,7 +120,8 @@ Few options can be provided alongside the initialization of the Language Service
|
|
|
119
120
|
"namespaceImportPackages": [], // package names that should be preferred as imported with namespace imports e.g. ["effect", "@effect/*"] (default: [])
|
|
120
121
|
"topLevelNamedReexports": "ignore", // for namespaceImportPackages, how should top level named re-exports (e.g. {pipe} from "effect") be treated? "ignore" will leave them as is, "follow" will rewrite them to the re-exported module (e.g. {pipe} from "effect/Function")
|
|
121
122
|
"importAliases": { "Array": "Arr" }, // allows to chose some different names for import name aliases (only when not chosing to import the whole module) (default: {})
|
|
122
|
-
"noExternal": false // disables features that provides links to external websites (such as links to mermaidchart.com) (default: false)
|
|
123
|
+
"noExternal": false, // disables features that provides links to external websites (such as links to mermaidchart.com) (default: false)
|
|
124
|
+
"keyPatterns": [{ "target": "service", "pattern": "default", "skipLeadingPath": ["src/"] }] // configure the key patterns; recommended reading more on the section "Configuring Key Patterns"
|
|
123
125
|
}
|
|
124
126
|
]
|
|
125
127
|
}
|
|
@@ -229,6 +231,56 @@ or you can set the severity for the entire project in the global plugin configur
|
|
|
229
231
|
}
|
|
230
232
|
```
|
|
231
233
|
|
|
234
|
+
## Configuring Key Patterns
|
|
235
|
+
|
|
236
|
+
Effect uses string keys for Services, Error Tags, RPC Methods, and more.
|
|
237
|
+
It can happen that sometimes, after some refactors or copy/paste, you may end up having wrong or non unique keys in your services.
|
|
238
|
+
|
|
239
|
+
To avoid that, the LSP suggests deterministic patterns for keys; that can be configured by the "keyPatterns" option.
|
|
240
|
+
|
|
241
|
+
To enable reporting of wrong or outdated keys, the rule "deterministicKeys" must be enabled first (off by default). To do so, adjust its diagnosticSeverity to error.
|
|
242
|
+
|
|
243
|
+
The keyPatterns key can then contain an array of the configured patterns.
|
|
244
|
+
|
|
245
|
+
```jsonc
|
|
246
|
+
{
|
|
247
|
+
"compilerOptions": {
|
|
248
|
+
"plugins": [
|
|
249
|
+
{
|
|
250
|
+
"name": "@effect/language-service",
|
|
251
|
+
// ...
|
|
252
|
+
"diagnosticSeverity": {
|
|
253
|
+
"deterministicKeys": "error" // enables reporting of wrong keys
|
|
254
|
+
// ...
|
|
255
|
+
},
|
|
256
|
+
"keyPatterns": [
|
|
257
|
+
{
|
|
258
|
+
"target": "service", // what key type to target between service|error
|
|
259
|
+
"pattern": "default", // the chosen pattern
|
|
260
|
+
"skipLeadingPath": ["src/"] // other pattern specific configs
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### Pattern: default
|
|
271
|
+
|
|
272
|
+
This pattern constructs keys by chaining package name + file path + class identifier.
|
|
273
|
+
|
|
274
|
+
E.g. `@effect/package/subpath-relative-to-package-root/FileName/ClassIdentifier`
|
|
275
|
+
|
|
276
|
+
If the filename and the class identifier are the same, they won't be repeated, but used only once.
|
|
277
|
+
|
|
278
|
+
The skipLeadingPath array can contain a set of prefixes to remove from the subpath part of the path. By default "src/" is removed for example.
|
|
279
|
+
|
|
280
|
+
### Pattern: package-identifier
|
|
281
|
+
|
|
282
|
+
This pattern uses the package name + identifier. This usually works great if you have a flat structure, with one file per service/error.
|
|
283
|
+
|
|
232
284
|
## Known gotchas
|
|
233
285
|
|
|
234
286
|
### Svelte VSCode extension and SvelteKit
|
|
@@ -1174,6 +1174,7 @@ var defaults = {
|
|
|
1174
1174
|
diagnosticSeverity: {},
|
|
1175
1175
|
quickinfo: true,
|
|
1176
1176
|
quickinfoEffectParameters: "whentruncated",
|
|
1177
|
+
quickinfoMaximumLength: -1,
|
|
1177
1178
|
completions: true,
|
|
1178
1179
|
goto: true,
|
|
1179
1180
|
inlays: true,
|
|
@@ -1183,8 +1184,25 @@ var defaults = {
|
|
|
1183
1184
|
barrelImportPackages: [],
|
|
1184
1185
|
importAliases: {},
|
|
1185
1186
|
renames: true,
|
|
1186
|
-
noExternal: false
|
|
1187
|
+
noExternal: false,
|
|
1188
|
+
keyPatterns: [{
|
|
1189
|
+
target: "service",
|
|
1190
|
+
pattern: "default",
|
|
1191
|
+
skipLeadingPath: ["src/"]
|
|
1192
|
+
}]
|
|
1187
1193
|
};
|
|
1194
|
+
function parseKeyPatterns(patterns) {
|
|
1195
|
+
const result = [];
|
|
1196
|
+
for (const entry of patterns) {
|
|
1197
|
+
if (!isObject(entry)) continue;
|
|
1198
|
+
result.push({
|
|
1199
|
+
target: hasProperty(entry, "target") && isString(entry.target) && ["service", "error"].includes(entry.target.toLowerCase()) ? entry.target.toLowerCase() : "service",
|
|
1200
|
+
pattern: hasProperty(entry, "pattern") && isString(entry.pattern) && ["package-identifier", "default"].includes(entry.pattern.toLowerCase()) ? entry.pattern.toLowerCase() : "default",
|
|
1201
|
+
skipLeadingPath: hasProperty(entry, "skipLeadingPath") && isArray(entry.skipLeadingPath) && entry.skipLeadingPath.every(isString) ? entry.skipLeadingPath : ["src/"]
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
return result;
|
|
1205
|
+
}
|
|
1188
1206
|
function parse(config) {
|
|
1189
1207
|
return {
|
|
1190
1208
|
refactors: isObject(config) && hasProperty(config, "refactors") && isBoolean(config.refactors) ? config.refactors : defaults.refactors,
|
|
@@ -1192,6 +1210,7 @@ function parse(config) {
|
|
|
1192
1210
|
diagnosticSeverity: isObject(config) && hasProperty(config, "diagnosticSeverity") && isRecord(config.diagnosticSeverity) ? parseDiagnosticSeverity(config.diagnosticSeverity) : defaults.diagnosticSeverity,
|
|
1193
1211
|
quickinfo: isObject(config) && hasProperty(config, "quickinfo") && isBoolean(config.quickinfo) ? config.quickinfo : defaults.quickinfo,
|
|
1194
1212
|
quickinfoEffectParameters: isObject(config) && hasProperty(config, "quickinfoEffectParameters") && isString(config.quickinfoEffectParameters) && ["always", "never", "whentruncated"].includes(config.quickinfoEffectParameters.toLowerCase()) ? config.quickinfoEffectParameters.toLowerCase() : defaults.quickinfoEffectParameters,
|
|
1213
|
+
quickinfoMaximumLength: isObject(config) && hasProperty(config, "quickinfoMaximumLength") && isNumber(config.quickinfoMaximumLength) ? config.quickinfoMaximumLength : defaults.quickinfoMaximumLength,
|
|
1195
1214
|
completions: isObject(config) && hasProperty(config, "completions") && isBoolean(config.completions) ? config.completions : defaults.completions,
|
|
1196
1215
|
goto: isObject(config) && hasProperty(config, "goto") && isBoolean(config.goto) ? config.goto : defaults.goto,
|
|
1197
1216
|
inlays: isObject(config) && hasProperty(config, "inlays") && isBoolean(config.inlays) ? config.inlays : defaults.inlays,
|
|
@@ -1201,7 +1220,8 @@ function parse(config) {
|
|
|
1201
1220
|
importAliases: isObject(config) && hasProperty(config, "importAliases") && isRecord(config.importAliases) ? map2(config.importAliases, (value) => String(value)) : defaults.importAliases,
|
|
1202
1221
|
topLevelNamedReexports: isObject(config) && hasProperty(config, "topLevelNamedReexports") && isString(config.topLevelNamedReexports) && ["ignore", "follow"].includes(config.topLevelNamedReexports.toLowerCase()) ? config.topLevelNamedReexports.toLowerCase() : defaults.topLevelNamedReexports,
|
|
1203
1222
|
renames: isObject(config) && hasProperty(config, "renames") && isBoolean(config.renames) ? config.renames : defaults.renames,
|
|
1204
|
-
noExternal: isObject(config) && hasProperty(config, "noExternal") && isBoolean(config.noExternal) ? config.noExternal : defaults.noExternal
|
|
1223
|
+
noExternal: isObject(config) && hasProperty(config, "noExternal") && isBoolean(config.noExternal) ? config.noExternal : defaults.noExternal,
|
|
1224
|
+
keyPatterns: isObject(config) && hasProperty(config, "keyPatterns") && isArray(config.keyPatterns) ? parseKeyPatterns(config.keyPatterns) : defaults.keyPatterns
|
|
1205
1225
|
};
|
|
1206
1226
|
}
|
|
1207
1227
|
|
|
@@ -3460,6 +3480,126 @@ var classSelfMismatch = createDiagnostic({
|
|
|
3460
3480
|
})
|
|
3461
3481
|
});
|
|
3462
3482
|
|
|
3483
|
+
// src/core/KeyBuilder.ts
|
|
3484
|
+
var makeKeyBuilder = fn("KeyBuilder")(
|
|
3485
|
+
function* (sourceFile) {
|
|
3486
|
+
const ts = yield* service(TypeScriptApi);
|
|
3487
|
+
const tsUtils = yield* service(TypeScriptUtils);
|
|
3488
|
+
const program = yield* service(TypeScriptProgram);
|
|
3489
|
+
const options = yield* service(LanguageServicePluginOptions);
|
|
3490
|
+
const packageInfo = tsUtils.resolveModuleWithPackageInfoFromSourceFile(program, sourceFile);
|
|
3491
|
+
function createString2(classNameText, kind) {
|
|
3492
|
+
if (!packageInfo) return;
|
|
3493
|
+
for (const keyPattern of options.keyPatterns) {
|
|
3494
|
+
if (keyPattern.target !== kind) continue;
|
|
3495
|
+
if (keyPattern.pattern === "package-identifier") {
|
|
3496
|
+
return packageInfo.name + "/" + classNameText;
|
|
3497
|
+
}
|
|
3498
|
+
const dirPath = getDirectoryPath(ts, sourceFile.fileName);
|
|
3499
|
+
if (!dirPath.startsWith(packageInfo.packageDirectory)) return;
|
|
3500
|
+
let subDirectory = dirPath.slice(packageInfo.packageDirectory.length);
|
|
3501
|
+
if (subDirectory.startsWith("/")) subDirectory = subDirectory.slice(1);
|
|
3502
|
+
const lastIndex = sourceFile.fileName.lastIndexOf("/");
|
|
3503
|
+
let subModule = lastIndex === -1 ? "" : sourceFile.fileName.slice(lastIndex + 1);
|
|
3504
|
+
for (const extension of [".ts", ".tsx", ".js", ".jsx"]) {
|
|
3505
|
+
if (subModule.toLowerCase().endsWith(extension)) {
|
|
3506
|
+
subModule = subModule.slice(0, -extension.length);
|
|
3507
|
+
break;
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
if (subModule.toLowerCase().endsWith("/index")) subModule = subModule.slice(0, -6);
|
|
3511
|
+
if (subModule.startsWith("/")) subModule = subModule.slice(1);
|
|
3512
|
+
for (const prefix of keyPattern.skipLeadingPath) {
|
|
3513
|
+
if (subDirectory.startsWith(prefix)) {
|
|
3514
|
+
subDirectory = subDirectory.slice(prefix.length);
|
|
3515
|
+
break;
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
const parts = [packageInfo.name, subDirectory, subModule].concat(
|
|
3519
|
+
subModule.toLowerCase() === classNameText.toLowerCase() ? [] : [classNameText]
|
|
3520
|
+
);
|
|
3521
|
+
return parts.filter((_) => String(_).trim().length > 0).join("/");
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
return {
|
|
3525
|
+
createString: createString2
|
|
3526
|
+
};
|
|
3527
|
+
}
|
|
3528
|
+
);
|
|
3529
|
+
var keyBuilderCache = /* @__PURE__ */ new Map();
|
|
3530
|
+
var getOrMakeKeyBuilder = fn("getOrMakeKeyBuilder")(function* (sourceFile) {
|
|
3531
|
+
const keyBuilder = keyBuilderCache.get(sourceFile.fileName) || (yield* makeKeyBuilder(sourceFile));
|
|
3532
|
+
keyBuilderCache.set(sourceFile.fileName, keyBuilder);
|
|
3533
|
+
return keyBuilder;
|
|
3534
|
+
});
|
|
3535
|
+
function createString(sourceFile, identifier, kind) {
|
|
3536
|
+
return map4(
|
|
3537
|
+
getOrMakeKeyBuilder(sourceFile),
|
|
3538
|
+
(identifierBuilder) => identifierBuilder.createString(identifier, kind)
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
// src/diagnostics/deterministicKeys.ts
|
|
3543
|
+
var deterministicKeys = createDiagnostic({
|
|
3544
|
+
name: "deterministicKeys",
|
|
3545
|
+
code: 25,
|
|
3546
|
+
severity: "off",
|
|
3547
|
+
apply: fn("deterministicKeys.apply")(function* (sourceFile, report) {
|
|
3548
|
+
const ts = yield* service(TypeScriptApi);
|
|
3549
|
+
const typeParser = yield* service(TypeParser);
|
|
3550
|
+
const nodeToVisit = [];
|
|
3551
|
+
const appendNodeToVisit = (node) => {
|
|
3552
|
+
nodeToVisit.push(node);
|
|
3553
|
+
return void 0;
|
|
3554
|
+
};
|
|
3555
|
+
ts.forEachChild(sourceFile, appendNodeToVisit);
|
|
3556
|
+
while (nodeToVisit.length > 0) {
|
|
3557
|
+
const node = nodeToVisit.shift();
|
|
3558
|
+
if (ts.isClassDeclaration(node) && node.name && node.heritageClauses) {
|
|
3559
|
+
const result = yield* pipe(
|
|
3560
|
+
pipe(
|
|
3561
|
+
typeParser.extendsEffectService(node),
|
|
3562
|
+
orElse2(() => typeParser.extendsContextTag(node)),
|
|
3563
|
+
orElse2(() => typeParser.extendsEffectTag(node)),
|
|
3564
|
+
map4(({ className, keyStringLiteral }) => ({ keyStringLiteral, className, target: "service" }))
|
|
3565
|
+
),
|
|
3566
|
+
orElse2(
|
|
3567
|
+
() => pipe(
|
|
3568
|
+
typeParser.extendsDataTaggedError(node),
|
|
3569
|
+
orElse2(() => typeParser.extendsSchemaTaggedError(node)),
|
|
3570
|
+
map4(({ className, keyStringLiteral }) => ({ keyStringLiteral, className, target: "error" }))
|
|
3571
|
+
)
|
|
3572
|
+
),
|
|
3573
|
+
orElse2(() => void_)
|
|
3574
|
+
);
|
|
3575
|
+
if (result && result.keyStringLiteral) {
|
|
3576
|
+
const { className, keyStringLiteral } = result;
|
|
3577
|
+
const classNameText = ts.idText(className);
|
|
3578
|
+
const expectedKey = yield* createString(sourceFile, classNameText, result.target);
|
|
3579
|
+
if (!expectedKey) return;
|
|
3580
|
+
const actualIdentifier = keyStringLiteral.text;
|
|
3581
|
+
if (actualIdentifier !== expectedKey) {
|
|
3582
|
+
report({
|
|
3583
|
+
location: keyStringLiteral,
|
|
3584
|
+
messageText: `Key should be '${expectedKey}'`,
|
|
3585
|
+
fixes: [{
|
|
3586
|
+
fixName: "deterministicKeys_fix",
|
|
3587
|
+
description: `Replace '${actualIdentifier}' with '${expectedKey}'`,
|
|
3588
|
+
apply: gen(function* () {
|
|
3589
|
+
const changeTracker = yield* service(ChangeTracker);
|
|
3590
|
+
const newStringLiteral = ts.factory.createStringLiteral(expectedKey);
|
|
3591
|
+
changeTracker.replaceNode(sourceFile, keyStringLiteral, newStringLiteral);
|
|
3592
|
+
})
|
|
3593
|
+
}]
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
ts.forEachChild(node, appendNodeToVisit);
|
|
3599
|
+
}
|
|
3600
|
+
})
|
|
3601
|
+
});
|
|
3602
|
+
|
|
3463
3603
|
// src/diagnostics/duplicatePackage.ts
|
|
3464
3604
|
var checkedPackagesCache = /* @__PURE__ */ new Map();
|
|
3465
3605
|
var programResolvedCacheSize = /* @__PURE__ */ new Map();
|
|
@@ -5469,7 +5609,8 @@ var diagnostics = [
|
|
|
5469
5609
|
outdatedEffectCodegen,
|
|
5470
5610
|
overriddenSchemaConstructor,
|
|
5471
5611
|
unsupportedServiceAccessors,
|
|
5472
|
-
nonObjectEffectServiceType
|
|
5612
|
+
nonObjectEffectServiceType,
|
|
5613
|
+
deterministicKeys
|
|
5473
5614
|
];
|
|
5474
5615
|
|
|
5475
5616
|
// src/effect-lsp-patch-utils.ts
|