-
Notifications
You must be signed in to change notification settings - Fork 6
Support programmatic linting, remove FS-centric bundle management #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
- Interface/API updates to support missing use cases and unexpected constraints - Test updates for all of the new and updated functionality
@@ -10,6 +10,17 @@ | |||
module.exports = { | |||
plugins: ['@salesforce/lwc-graph-analyzer'], | |||
processor: '@salesforce/lwc-graph-analyzer/bundleAnalyzer', | |||
parser: '@babel/eslint-parser', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Without this, every project that included our plugin also had to include one of the LWC ESLint configs from eslint-config-lwc
, to get the parser that would accept LWC code with decorators.
@@ -8,6 +8,8 @@ | |||
'use strict'; | |||
|
|||
const { createRule } = require('../util/createRule'); | |||
const ruleDefinition = createRule(__filename); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
__filename
is problematic in ESBuild bundles. Which stands to reason, as ESBuild bundles all code into one file, making filenames largely irrelevant.
* @param {string | undefined} jsContent - The content of the JS file | ||
* @param {string[]} htmlTemplateContent - The contents of the HTML templates | ||
*/ | ||
setLwcBundleFromContent = (componentBaseName, jsContent, ...htmlTemplateContent) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setLwcBundleFromContent()
is the initialization of the programmatic use case for calling our plugin. It basically looks like this, from a consuming project:
import { Linter } from "eslint";
import lwcGraphAnalyzerPlugin, {
LwcBundle,
} from "@salesforce/eslint-plugin-lwc-graph-analyzer";
function main() {
const jsToAnalyze = `
import { LightningElement } from 'lwc';
export default class Example extends LightningElement {
prop;
get input() {
this.prop = 'hi';
return this.prop;
}
}
`;
const bundleAnalyzer = lwcGraphAnalyzerPlugin.processors.bundleAnalyzer;
bundleAnalyzer.setLwcBundleFromContent(
"myFaultyComp",
jsToAnalyze,
// You'd add HTML template files one by one here, if they're a part of your bundle.
);
const linter = new Linter();
const recommendedConfig = lwcGraphAnalyzerPlugin.configs.recommended;
const messages = linter.verifyAndFix(jsToAnalyze, {
plugins: {
"@salesforce/lwc-graph-analyzer": lwcGraphAnalyzerPlugin,
},
rules: recommendedConfig.rules,
processor: bundleAnalyzer,
});
console.log("messages:");
console.log(`${JSON.stringify(messages, null, 2)}\n`);
}
main();
* by the ESLint CLI, or a dummy value (__placeholder__.js) if called programatically. | ||
* @returns {{ text: string, filename: string }[]} An array of files and their content, to be processed. | ||
*/ | ||
preprocess = (text, filename) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
preprocess()
is the logical heart of the custom processor. Everything you see in this class basically gets tapped out of this workflow, other than pre-setting this.#lwcBundle
from outside.
Note: Because of the way ESLint strips preprocess
and postprocess
methods out of their defining context before it runs them, I either had to use arrow functions like these, or do some explicit binding stuff in the class, to allow the functions to make use of this
directives. I opted for arrow functions.
* by the ESLint CLI, or a dummy value (__placeholder__.js) if called programatically. | ||
* @returns {{ text: string, filename: string }[]} An array of files and their content, to be processed. | ||
*/ | ||
preprocess = (text, filename) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a basic flow through the method. Key points:
- If
this.#lwcBundle
has been set by the consumer, that's the bundle that will get processed. - If not, it's assumed that the input content is from an LWC on the filesystem, and it creates an
LwcBundle
based interrogating those contents. - If it's not on the filesystem either, it creates a one-file bundle out of the incoming file.
- The concept of "primary file" is key to the storage and retrieval of the bundle: the file coming into
preprocess()
is the "primary file": the file being operated on. Any other files are just supporting bundle files to analysis. The primary file keys the in-memory bundle, for later retrieval by the rules-based analysis.
graph TD
A[Start preprocess] --> B{Is lwcBundle set?}
B -->|No| C[Try to create bundle from filesystem]
B -->|Yes| D[Set primary file by content]
C --> E{Bundle created successfully?}
E -->|Yes| F[Use filesystem bundle - sets primary file]
E -->|No| G[Create single file bundle - sets primary file]
D --> H{Found matching file?}
H -->|Yes| I[Continue processing]
H -->|No| J[Return empty array]
F --> I
G --> I
I --> K[Cache bundle]
K --> L{Is file .js?}
L -->|Yes| M[Return original text]
L -->|No| N[Return empty text]
M --> O[End]
N --> O
J --> O
* | ||
* @returns {string} A unique key in the format `<base file name>_<uuid>.<extension>` | ||
*/ | ||
getBundleKey() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We couldn't just use the filename, because it may not be unique between bundles. I originally was using a hash of the content, only to realize that when you zero out the content of the HTML file to pass it through, that's what ends up on the other side in the analysis, so no way to track it. Appending a unique identifier to the filename seemed the best way, on the only field that tracks from processing to analysis.
* @param {string} eslintFilename - The filename as provided by ESLint's context | ||
* @returns {string} The original bundle key used for lookup | ||
*/ | ||
function extractBundleKey(eslintFilename) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ESLint makes some wonky path out of the filename that you create in the processor. So you have to undo that here—there's no other direct link or method to give you back the filename you specified.
if (ext === '.html') { | ||
return [{ text: '', filename }]; | ||
|
||
setLwcBundleCacheEntry(this.#lwcBundle); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This adds the LwcBundle
to the in-memory cache to pick up on the rules analysis side.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to understand more about BundleStateManager
/cache a bit. When the processor runs wouldn't it process a bundle at a time? The way it is cached with BundleStateManager
the code reads to me as though processing bundles is not serial but a parallel processing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know enough about the internals of how ESLint processes multiple files as input (e.g. eslint components/*.js
), so I didn't want to run the risk of one bundle clashing with another, if ESLint adds any level of parallel processing when it gets multiple files as input.
/** | ||
* Interface for filesystem operations | ||
*/ | ||
class FilesystemProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm guessing class
is used here because Javascript doesn't have interface like Typescript. And that you wanted to create something like abstract class in C++, which is like interface? And you wanted to create a layer of abstraction to be able to create mockups for tests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly. In TypeScript I would have made this an interface
, because its intention is to support dependency injection. This felt like the best approximation that allowed clear visibility to developers, into the intent of describing an API shape that otherwise must be implemented, not executed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! This modification to the plugin was a huge undertaking!
What does this PR do?
lwcBundle
property on the processor (BundleAnalyzer
).BundleAnalyzer.preprocess()
will look for this property, and fall back to the original filesystem interrogation if it's not set.@babel/eslint-parser
in ourbase
configuration instead.I'll add more commentary inline, wherever it makes sense.
What issues does this PR fix or reference?