Skip to content
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

Prototype a stylelint shim #7464

Closed
wants to merge 1 commit into from
Closed

Conversation

jesstelford
Copy link
Contributor

@jesstelford jesstelford commented Oct 20, 2022

tl;dr: I wrote a shim which should simplify future work to improve the coverage tool. It'll help with code maintenance today, with the goal of making our coverage more accurate and the stylelint-polaris tool more useful in the future. See the migration template for a minimal usage example.


In conversation with @aaronccasanova yesterday, he pointed out that except for some extreme edge cases, all our migrations follow an identical pattern of:

  1. Check if a given CSS line is something the migration cares about ('spacing' doesn't care about display: declarations, for example)
  2. Identify the "failure"
  3. Try to fix it
  4. Note when we can't automatically fix it.

In seeing that pattern, he's been able to simplify logic and converge on a nice code structure as exemplified by the replace-spacing-lengths migration.

I began shoring up the motion migration to follow this structure and started to see that @aaronccasanova's structure could be extended further to remove duplication and make every migration even more consistent + easy to maintain.

At the same time, I've been thinking on how our coverage tooling (powered by stylelint) and migration tooling (powered by postcss) relate, and specifically how they're starting to diverge, and wondering what we can do about it. I discovered that stylelint supports running "migrations" via its built-in --fix option. I immediately thought of a future where we used stylelint to execute our postcss migrations.

It turns out stylelint is actually powered by postcss! And the the way Stylelint rules function is:

  1. Check if a given CSS line is something the lint rule cares about ('spacing' doesn't care about display: declarations, for example)
  2. Identify any linting "errors" or "warnings"
  3. Optionally; Try to fix it when --fix is passed

Which is almost identical to @aaronccasanova's observation and subsequent code structure 🤯

So, much like Alice, I tumbled down the Rabbit Hole :rabbit-hole: and realised that with just a little shim function, we could write our postcss migrations today such that they're compatible with being run within stylelint in the future, while ensuring the migrations are consistent and easy to maintain ✅

I'd love to hear thoughts! Is this useful? Is it confusing? Am I aiming in the right direction?

// padding: var(--p-space-4) 10rem;
padding: 16px 10rem;
// polaris-migrator: Unable to migrate the following expression. Please upgrade manually.
// error: Non-tokenizable value '10px'
// padding: 10px var(--p-space-4);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still outputs the partial fix where possible, but enhances that with the specific reason it couldn't be fully fixed.

Huge DX win!

@@ -23,55 +25,65 @@
padding: var(--p-space-4, 16px);
// Comment
// polaris-migrator: Unable to migrate the following expression. Please upgrade manually.
// padding: 10px;
// error: Non-tokenizable value '10px'
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These error messages will be the exact same as exposed by stylelint once we migrate there. 🎉

);
} else {
decl.value = parsedValue.toString();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Begone repeated boilerplate code! 🪄

@BPScott
Copy link
Member

BPScott commented Oct 20, 2022

I think my question is: In what cases would we want migration checks and fixes to exist in perpetuity?

Stylelint rules hang around forever - they spot mistakes that could be made and fix them. However the modifications in migrations tend to be one and done. You apply a migration like "replace usage of spacing() with tokens" and then you delete the spacing() function from your codebase as it is no longer used. At that point if you ever try to use the spacing() function it is an error because that function no longer exists. Web already enables the scss/no-global-function-names lint rule that warns against doing spacing() (which would compile but silently put spacing() in your compiled scss), and because the function no longer exists legacy-polaris-v8.spacing() would result in a compile time error. What extra value does a lint rule that says "you can't use spacing()" provide that is not already covered by that status quo?

That said, if the plan is "We stop using codemod for migrations, instead these fixes are exposed as a stylelint rules that contains autofixes where appropriate" then that sounds legit.

@jesstelford
Copy link
Contributor Author

@BPScott Yes, this is a fantastic question!

There's really two distinct times a developer might want their code automatically fixed for them:

  1. When working in their IDE, and an auto-fix suggestion says "Hey, that could be a Polaris token. Click to fix."
  2. When doing Polaris major version migrations.

For #1, we have stylelint-polaris, which implements some basic warnings, but cannot fix them.

For #2, we have polaris-migrator, which can fix issues based on more complex logic than the stylelint-polaris, but cannot be used in an IDE.

These two use-cases are distinct, but it turns out they can use the exact same tech stack: stylelint rules. This allows detection and fixing whether it's from an IDE or from a migrator cli.

The "one and done" migrations would be separate rules (bundled into 8-to-9 style presets) compared to the "fix this in my IDE" rules. Stylelint also allows executing rules from within other rules, so a migration may leverage the same rules as an IDE one, or vice versa.

All of that is future-looking. This PR is mearly the first step in that direction (one I hope I'm aligned with the rest of the team on 😅)

@sam-b-rose sam-b-rose mentioned this pull request Oct 20, 2022
4 tasks
@jesstelford jesstelford force-pushed the prototype-stylelint-shim branch from 07dca91 to bfa85dc Compare October 24, 2022 13:21

// Expose a stylelint-like API for creating sass migrators so we can easily
// migrate to that tool in the future.
function convertStylelintRuleToPostcssProcessor(ruleFn: StylelintRule) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the shim function.

Approx 90% of this function would be deleted by migrating to stylelint, meaning this function is just a shim to get postcss's API to look like stylelint's (for now).

partialFixStrategy: 'report',
__injectReportsAsComments: true,
},
{fix: true},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix option is passed by stylelint when the cli switch --fix is passed. Since this is within our migrator cli, we hard-code it to true for this shim.

{
...options,
partialFixStrategy: 'report',
__injectReportsAsComments: true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example of receiving options via stylelint rule config. In this case, we're instructing the migrations to insert comments (because that's the only way to get output from postcss), but it could also be set to emit stylelint "reports" which are then consumable by other tools such as LSPs, VSCode, etc.

(existingReport) =>
existingReport.severity === newReport.severity &&
existingReport.message === newReport.message,
) === -1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deduping reports to avoid noise. Particularly useful for example padding: 10px 10px 10px 10px - we only want 1 warning, not 4 identical ones.

};
}

export function createStylelintRule(ruleName: string, ruleFn: PolarisMigrator) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This factory function matches the stylelint API as closely as possible. There are only a couple of lines in this function which would be removed once we move to stylelint.

| ((node: T, parsedValue: ParsedValue) => false | void);
mutableKeys?: (keyof T)[];
serialiseSuggestion: (node: T) => string;
}): (node: T) => false | void {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typescript 😭

mutableKeys?: (keyof T)[];
serialiseSuggestion: (node: T) => string;
}): (node: T) => false | void;
function createWalker<T extends PostCSSNode>(args: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This factory function wraps up common logic across all of our migrators:

  • Tracking if changes have occurred to a postcss Node (previously done manually in each migration via a targets array)
  • Adding comments if there's a partial migration
  • Add comments when there's an error/warning detected, but it's unmigratable

walkAtRules,
walkComments,
walkDecls,
walkRules,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These util methods cover most of the postcss Root API that our migrations will/might use.

@jesstelford jesstelford marked this pull request as ready for review October 24, 2022 13:38
@sam-b-rose
Copy link
Member

sam-b-rose commented Oct 24, 2022

This is an excellent exploration aligning our migration structure with how Stylelint structures rules. This iteration does bring the abstraction closer to Stylelint's API, but it also introduces a lot of complexity and changes to our current migrations–even if only temporary.

Until we are ready and sure we'd like to migrate the migrations to our Stylelint plugin, I'd like to let this work simmer if that is alright 🫕. This will certainly be valuable in helping form our strategy for integrating stylelint-polaris with Shopify/web 👍

@jesstelford jesstelford force-pushed the prototype-stylelint-shim branch from 268804c to 72cb3c3 Compare October 25, 2022 05:35
@jesstelford
Copy link
Contributor Author

Fair call: This PR has a lot going on. I'll split out a couple things I think will be useful for us to have in separate PRs 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants