@effect/language-service 0.42.0 → 0.43.1
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 +52 -1
- package/effect-lsp-patch-utils.js +144 -3
- package/effect-lsp-patch-utils.js.map +1 -1
- package/index.js +421 -275
- 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
|
@@ -120,7 +120,8 @@ Few options can be provided alongside the initialization of the Language Service
|
|
|
120
120
|
"namespaceImportPackages": [], // package names that should be preferred as imported with namespace imports e.g. ["effect", "@effect/*"] (default: [])
|
|
121
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")
|
|
122
122
|
"importAliases": { "Array": "Arr" }, // allows to chose some different names for import name aliases (only when not chosing to import the whole module) (default: {})
|
|
123
|
-
"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"
|
|
124
125
|
}
|
|
125
126
|
]
|
|
126
127
|
}
|
|
@@ -230,6 +231,56 @@ or you can set the severity for the entire project in the global plugin configur
|
|
|
230
231
|
}
|
|
231
232
|
```
|
|
232
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
|
+
|
|
233
284
|
## Known gotchas
|
|
234
285
|
|
|
235
286
|
### Svelte VSCode extension and SvelteKit
|
|
@@ -1184,8 +1184,25 @@ var defaults = {
|
|
|
1184
1184
|
barrelImportPackages: [],
|
|
1185
1185
|
importAliases: {},
|
|
1186
1186
|
renames: true,
|
|
1187
|
-
noExternal: false
|
|
1187
|
+
noExternal: false,
|
|
1188
|
+
keyPatterns: [{
|
|
1189
|
+
target: "service",
|
|
1190
|
+
pattern: "default",
|
|
1191
|
+
skipLeadingPath: ["src/"]
|
|
1192
|
+
}]
|
|
1188
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
|
+
}
|
|
1189
1206
|
function parse(config) {
|
|
1190
1207
|
return {
|
|
1191
1208
|
refactors: isObject(config) && hasProperty(config, "refactors") && isBoolean(config.refactors) ? config.refactors : defaults.refactors,
|
|
@@ -1203,7 +1220,8 @@ function parse(config) {
|
|
|
1203
1220
|
importAliases: isObject(config) && hasProperty(config, "importAliases") && isRecord(config.importAliases) ? map2(config.importAliases, (value) => String(value)) : defaults.importAliases,
|
|
1204
1221
|
topLevelNamedReexports: isObject(config) && hasProperty(config, "topLevelNamedReexports") && isString(config.topLevelNamedReexports) && ["ignore", "follow"].includes(config.topLevelNamedReexports.toLowerCase()) ? config.topLevelNamedReexports.toLowerCase() : defaults.topLevelNamedReexports,
|
|
1205
1222
|
renames: isObject(config) && hasProperty(config, "renames") && isBoolean(config.renames) ? config.renames : defaults.renames,
|
|
1206
|
-
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
|
|
1207
1225
|
};
|
|
1208
1226
|
}
|
|
1209
1227
|
|
|
@@ -3462,6 +3480,128 @@ var classSelfMismatch = createDiagnostic({
|
|
|
3462
3480
|
})
|
|
3463
3481
|
});
|
|
3464
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
|
+
const lastIndex = sourceFile.fileName.lastIndexOf("/");
|
|
3496
|
+
let onlyFileName = lastIndex === -1 ? "" : sourceFile.fileName.slice(lastIndex + 1);
|
|
3497
|
+
const lastExtensionIndex = onlyFileName.lastIndexOf(".");
|
|
3498
|
+
if (lastExtensionIndex !== -1) onlyFileName = onlyFileName.slice(0, lastExtensionIndex);
|
|
3499
|
+
if (onlyFileName.toLowerCase().endsWith("/index")) onlyFileName = onlyFileName.slice(0, -6);
|
|
3500
|
+
if (onlyFileName.startsWith("/")) onlyFileName = onlyFileName.slice(1);
|
|
3501
|
+
let subDirectory = getDirectoryPath(ts, sourceFile.fileName);
|
|
3502
|
+
if (!subDirectory.startsWith(packageInfo.packageDirectory)) continue;
|
|
3503
|
+
subDirectory = subDirectory.slice(packageInfo.packageDirectory.length);
|
|
3504
|
+
if (!subDirectory.endsWith("/")) subDirectory = subDirectory + "/";
|
|
3505
|
+
if (subDirectory.startsWith("/")) subDirectory = subDirectory.slice(1);
|
|
3506
|
+
for (const prefix of keyPattern.skipLeadingPath) {
|
|
3507
|
+
if (subDirectory.startsWith(prefix)) {
|
|
3508
|
+
subDirectory = subDirectory.slice(prefix.length);
|
|
3509
|
+
break;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
let parts = [packageInfo.name, subDirectory, onlyFileName].concat(
|
|
3513
|
+
onlyFileName.toLowerCase() === classNameText.toLowerCase() ? [] : [classNameText]
|
|
3514
|
+
);
|
|
3515
|
+
if (keyPattern.pattern === "package-identifier") {
|
|
3516
|
+
parts = [packageInfo.name, onlyFileName].concat(
|
|
3517
|
+
onlyFileName.toLowerCase() === classNameText.toLowerCase() ? [] : [classNameText]
|
|
3518
|
+
);
|
|
3519
|
+
}
|
|
3520
|
+
parts = parts.map((part) => part.startsWith("/") ? part.slice(1) : part).map(
|
|
3521
|
+
(part) => part.endsWith("/") ? part.slice(0, -1) : part
|
|
3522
|
+
);
|
|
3523
|
+
return parts.filter((_) => String(_).trim().length > 0).join("/");
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
return {
|
|
3527
|
+
createString: createString2
|
|
3528
|
+
};
|
|
3529
|
+
}
|
|
3530
|
+
);
|
|
3531
|
+
var keyBuilderCache = /* @__PURE__ */ new Map();
|
|
3532
|
+
var getOrMakeKeyBuilder = fn("getOrMakeKeyBuilder")(function* (sourceFile) {
|
|
3533
|
+
const keyBuilder = keyBuilderCache.get(sourceFile.fileName) || (yield* makeKeyBuilder(sourceFile));
|
|
3534
|
+
keyBuilderCache.set(sourceFile.fileName, keyBuilder);
|
|
3535
|
+
return keyBuilder;
|
|
3536
|
+
});
|
|
3537
|
+
function createString(sourceFile, identifier, kind) {
|
|
3538
|
+
return map4(
|
|
3539
|
+
getOrMakeKeyBuilder(sourceFile),
|
|
3540
|
+
(identifierBuilder) => identifierBuilder.createString(identifier, kind)
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
// src/diagnostics/deterministicKeys.ts
|
|
3545
|
+
var deterministicKeys = createDiagnostic({
|
|
3546
|
+
name: "deterministicKeys",
|
|
3547
|
+
code: 25,
|
|
3548
|
+
severity: "off",
|
|
3549
|
+
apply: fn("deterministicKeys.apply")(function* (sourceFile, report) {
|
|
3550
|
+
const ts = yield* service(TypeScriptApi);
|
|
3551
|
+
const typeParser = yield* service(TypeParser);
|
|
3552
|
+
const nodeToVisit = [];
|
|
3553
|
+
const appendNodeToVisit = (node) => {
|
|
3554
|
+
nodeToVisit.push(node);
|
|
3555
|
+
return void 0;
|
|
3556
|
+
};
|
|
3557
|
+
ts.forEachChild(sourceFile, appendNodeToVisit);
|
|
3558
|
+
while (nodeToVisit.length > 0) {
|
|
3559
|
+
const node = nodeToVisit.shift();
|
|
3560
|
+
if (ts.isClassDeclaration(node) && node.name && node.heritageClauses) {
|
|
3561
|
+
const result = yield* pipe(
|
|
3562
|
+
pipe(
|
|
3563
|
+
typeParser.extendsEffectService(node),
|
|
3564
|
+
orElse2(() => typeParser.extendsContextTag(node)),
|
|
3565
|
+
orElse2(() => typeParser.extendsEffectTag(node)),
|
|
3566
|
+
map4(({ className, keyStringLiteral }) => ({ keyStringLiteral, className, target: "service" }))
|
|
3567
|
+
),
|
|
3568
|
+
orElse2(
|
|
3569
|
+
() => pipe(
|
|
3570
|
+
typeParser.extendsDataTaggedError(node),
|
|
3571
|
+
orElse2(() => typeParser.extendsSchemaTaggedError(node)),
|
|
3572
|
+
map4(({ className, keyStringLiteral }) => ({ keyStringLiteral, className, target: "error" }))
|
|
3573
|
+
)
|
|
3574
|
+
),
|
|
3575
|
+
orElse2(() => void_)
|
|
3576
|
+
);
|
|
3577
|
+
if (result && result.keyStringLiteral) {
|
|
3578
|
+
const { className, keyStringLiteral } = result;
|
|
3579
|
+
const classNameText = ts.idText(className);
|
|
3580
|
+
const expectedKey = yield* createString(sourceFile, classNameText, result.target);
|
|
3581
|
+
if (!expectedKey) return;
|
|
3582
|
+
const actualIdentifier = keyStringLiteral.text;
|
|
3583
|
+
if (actualIdentifier !== expectedKey) {
|
|
3584
|
+
report({
|
|
3585
|
+
location: keyStringLiteral,
|
|
3586
|
+
messageText: `Key should be '${expectedKey}'`,
|
|
3587
|
+
fixes: [{
|
|
3588
|
+
fixName: "deterministicKeys_fix",
|
|
3589
|
+
description: `Replace '${actualIdentifier}' with '${expectedKey}'`,
|
|
3590
|
+
apply: gen(function* () {
|
|
3591
|
+
const changeTracker = yield* service(ChangeTracker);
|
|
3592
|
+
const newStringLiteral = ts.factory.createStringLiteral(expectedKey);
|
|
3593
|
+
changeTracker.replaceNode(sourceFile, keyStringLiteral, newStringLiteral);
|
|
3594
|
+
})
|
|
3595
|
+
}]
|
|
3596
|
+
});
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
ts.forEachChild(node, appendNodeToVisit);
|
|
3601
|
+
}
|
|
3602
|
+
})
|
|
3603
|
+
});
|
|
3604
|
+
|
|
3465
3605
|
// src/diagnostics/duplicatePackage.ts
|
|
3466
3606
|
var checkedPackagesCache = /* @__PURE__ */ new Map();
|
|
3467
3607
|
var programResolvedCacheSize = /* @__PURE__ */ new Map();
|
|
@@ -5471,7 +5611,8 @@ var diagnostics = [
|
|
|
5471
5611
|
outdatedEffectCodegen,
|
|
5472
5612
|
overriddenSchemaConstructor,
|
|
5473
5613
|
unsupportedServiceAccessors,
|
|
5474
|
-
nonObjectEffectServiceType
|
|
5614
|
+
nonObjectEffectServiceType,
|
|
5615
|
+
deterministicKeys
|
|
5475
5616
|
];
|
|
5476
5617
|
|
|
5477
5618
|
// src/effect-lsp-patch-utils.ts
|