-
Notifications
You must be signed in to change notification settings - Fork 66
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
Add Support for Sorting Exports #43
Conversation
JCrepin
commented
May 18, 2020
- Off by default (enable via rule config)
- Only sorts exports after all imports
- Piggybacks off the existing import rules, with a few tweaks to get them to work with both imports and exports
- Basic unit tests included. I don't see anyone using these rules for much more than index files.
…rts + basic tests
Hi!
Do we really need an option? 🤔
What does this mean?
Yes, we should share as much as possible, but not do odd things like passing something that isn’t an import into
What does this mean? We have to do the right thing regardless of how users use exports? Is the idea to find all runs (chunks) of supported variants of Can you add all forms of Can using Can you look through some big open source projects and take examples of real-life exports from them? There are already some |
Hi, Thanks for getting back to me so quickly.
This depends on whether we want this to be a breaking change or not. Without it, we're forcing the rule, which may cause friction for large code bases that don't want to update existing code. Thoughts?
This means I'm only sorting exports that are after all the imports (everything prior is ignored). Actually, this should probably be removed, now that I think about it. It does lead into the question of if we'll want to introduce rules as to how exports sort in relation to imports though, as mentioned by @remcohaszing in #43. If you agree with those rules, I'll add them.
Hmm, the alternative for this example is to add conditional logic to
Yes, though as mentioned above, I'm only doing this after all imports, but it can be changed to process all exports in the file.
Both done.
Yes they can. The only issue I could see is if you have two side-effect imports that need to be run in a specific order that's not alphabetical. If you need to export these, then you'd need to disable the rule so we don't sort them. If you don't need to export them, you'd just do side-effect imports in the appropriate order before your exports. |
Why wouldn’t they? (There might be a legit reason, I just can’t think of it.) One way would be to release a major version without the option and add it later if people complain convincingly.
I haven’t looked at the code close enough. You can leave it as-is.
Yes, I think we should sort exports not matter where they are in the file (drop the “only after imports” thing).
I think we should keep the idea where each “chunk” of sortable statements are sorted individually. If you want to merge chunks you need a different rule, such as The only question is if imports and exports can be in the same chunk, or if they should be different chunks. I don’t think I’d be against letting imports and exports be in the same chunk, turning this: import a from "a"
export * as b from "b"
import c from "c" into this: import a from "a"
import c from "c"
export * as b from "b" If so, the rule would be “exports always come after all imports in a chunk”. One worry I have, though, is that this: import a from "a"
export * as b from "b"
export const x = 5
import c from "c" Would be unchanged (because Maybe it makes sense to let chunks include any import statements plus any export statements? If so, the rule could be:
Could that cause problems, like evaluation order stuff? I don’t think so, but I’m not sure 🤔 Have I understood it correctly that we only support sorting But do we also want to sort the things inside export { b, z as a, c }; We can’t move such an export statement itself, but we could sort the exported items. |
I actually meant re-exports should be located somewhere consistently after sorting. I didn’t mean the original location of the re-exports. I think code should always be sorted as imports first, then all re-exports, then anything else. I don’t think regular exports should be handled by this rule. So the following code: import a from "a" // import
export * as b from "b" // re-export
export const x = 5 // other
import c from "c" // import
// more other would be sorted as: import a from "a" // import
import c from "c" // import
export * as b from "b" // re-export
export const x = 5 // other
// more other |
Good idea with the blank lines between import and export. But should there be one between those exports? 🤔 What’s the rule? |
I don’t take exports into account for this rule, only re-exports ( In the example, Exports may appear in other places as well and may depend on other statements. A more complete example: import a from "a" // import
import c from "c" // import
export * as b from "b" // re-export
// anything below this line is unhandled
export const x = 5
console.log(a)
console.log(c)
export const y = x
export function foo() {
console.log(x);
}
export const bar = foo; This shows exports can’t be safely sorted. Only sorting exported |
Yes, that’s in line with how things already worked and what I proposed. |
I agree, so long as it's a major version bump, that works for me. The only reason someone wouldn't want to do this is the time it could take making the changes in large teams as they could run into merge conflicts. I've removed the config.
Done.
As we keep the original order of source-less exports, I don't see any issues here. We allow source-less exports to be in a chunk, though I've ensured a newline between exports and source-less exports. For example:
becomes:
Yeah, it makes sense to sort the specifiers here. I've made this change, but obviously it's just the specifiers we sort in this case- the export itself doesn't get sorted. |
Awesome work, I’ll have a look at it this weekend. |
I haven’t had the time to look into this as much as I had wanted, but I think we have explored everything we need to in this PR. Thanks! This is now om my list of things to do, and I would like continue the work from here myself the upcoming weeks if you don’t mind? |
By all means, go ahead. Thanks. |
Hi! Just wanted to let you know that I haven’t forgotten about this. Made some progress today on updating the readme and examples. |
Made some more progress on tests and review today. |
Here’s an example where the output of eslint-plugin-simple-import-sort kind of is worse, I think: // https://github.com/graphql/graphql-js/blob/f7061fdcf461a2e4b3c78077afaebefc2226c8e3/src/utilities/index.js#L1-L115
{
code: input`
|// @flow strict
|
|// Produce the GraphQL query recommended for a full schema introspection.
|// Accepts optional IntrospectionOptions.
|export { getIntrospectionQuery } from './getIntrospectionQuery';
|
|export type {
| IntrospectionOptions,
| IntrospectionQuery,
| IntrospectionSchema,
| IntrospectionType,
| IntrospectionInputType,
| IntrospectionOutputType,
| IntrospectionScalarType,
| IntrospectionObjectType,
| IntrospectionInterfaceType,
| IntrospectionUnionType,
| IntrospectionEnumType,
| IntrospectionInputObjectType,
| IntrospectionTypeRef,
| IntrospectionInputTypeRef,
| IntrospectionOutputTypeRef,
| IntrospectionNamedTypeRef,
| IntrospectionListTypeRef,
| IntrospectionNonNullTypeRef,
| IntrospectionField,
| IntrospectionInputValue,
| IntrospectionEnumValue,
| IntrospectionDirective,
|} from './getIntrospectionQuery';
|
|// Gets the target Operation from a Document.
|export { getOperationAST } from './getOperationAST';
|
|// Gets the Type for the target Operation AST.
|export { getOperationRootType } from './getOperationRootType';
|
|// Convert a GraphQLSchema to an IntrospectionQuery.
|export { introspectionFromSchema } from './introspectionFromSchema';
|
|// Build a GraphQLSchema from an introspection result.
|export { buildClientSchema } from './buildClientSchema';
|
|// Build a GraphQLSchema from GraphQL Schema language.
|export { buildASTSchema, buildSchema } from './buildASTSchema';
|export type { BuildSchemaOptions } from './buildASTSchema';
|
|// Extends an existing GraphQLSchema from a parsed GraphQL Schema language AST.
|export {
| extendSchema,
| // @deprecated: Get the description from a schema AST node and supports legacy
| // syntax for specifying descriptions - will be removed in v16.
| getDescription,
|} from './extendSchema';
|
|// Sort a GraphQLSchema.
|export { lexicographicSortSchema } from './lexicographicSortSchema';
|
|// Print a GraphQLSchema to GraphQL Schema language.
|export {
| printSchema,
| printType,
| printIntrospectionSchema,
|} from './printSchema';
|
|// Create a GraphQLType from a GraphQL language AST.
|export { typeFromAST } from './typeFromAST';
|
|// Create a JavaScript value from a GraphQL language AST with a type.
|export { valueFromAST } from './valueFromAST';
|
|// Create a JavaScript value from a GraphQL language AST without a type.
|export { valueFromASTUntyped } from './valueFromASTUntyped';
|
|// Create a GraphQL language AST from a JavaScript value.
|export { astFromValue } from './astFromValue';
|
|// A helper to use within recursive-descent visitors which need to be aware of
|// the GraphQL type system.
|export { TypeInfo, visitWithTypeInfo } from './TypeInfo';
|
|// Coerces a JavaScript value to a GraphQL type, or produces errors.
|export { coerceInputValue } from './coerceInputValue';
|
|// Concatenates multiple AST together.
|export { concatAST } from './concatAST';
|
|// Separates an AST into an AST per Operation.
|export { separateOperations } from './separateOperations';
|
|// Strips characters that are not significant to the validity or execution
|// of a GraphQL document.
|export { stripIgnoredCharacters } from './stripIgnoredCharacters';
|
|// Comparators for types
|export {
| isEqualType,
| isTypeSubTypeOf,
| doTypesOverlap,
|} from './typeComparators';
|
|// Asserts that a string is a valid GraphQL name
|export { assertValidName, isValidNameError } from './assertValidName';
|
|// Compares two GraphQLSchemas and detects breaking changes.
|export {
| BreakingChangeType,
| DangerousChangeType,
| findBreakingChanges,
| findDangerousChanges,
|} from './findBreakingChanges';
|export type { BreakingChange, DangerousChange } from './findBreakingChanges';
|
|// Report all deprecated usage within a GraphQL document.
|export { findDeprecatedUsages } from './findDeprecatedUsages';
`,
output: (actual) => {
expect(actual).toMatchInlineSnapshot(`
|// @flow strict
|
|// Produce the GraphQL query recommended for a full schema introspection.
|// Accepts optional IntrospectionOptions.
|// Asserts that a string is a valid GraphQL name
|export { assertValidName, isValidNameError } from './assertValidName';
|// Create a GraphQL language AST from a JavaScript value.
|export { astFromValue } from './astFromValue';
|export type { BuildSchemaOptions } from './buildASTSchema';
|// Build a GraphQLSchema from GraphQL Schema language.
|export { buildASTSchema, buildSchema } from './buildASTSchema';
|// Build a GraphQLSchema from an introspection result.
|export { buildClientSchema } from './buildClientSchema';
|// Coerces a JavaScript value to a GraphQL type, or produces errors.
|export { coerceInputValue } from './coerceInputValue';
|// Concatenates multiple AST together.
|export { concatAST } from './concatAST';
|// Extends an existing GraphQLSchema from a parsed GraphQL Schema language AST.
|export {
| extendSchema,
| // @deprecated: Get the description from a schema AST node and supports legacy
| // syntax for specifying descriptions - will be removed in v16.
| getDescription,
|} from './extendSchema';
|export type { BreakingChange, DangerousChange } from './findBreakingChanges';
|// Compares two GraphQLSchemas and detects breaking changes.
|export {
| BreakingChangeType,
| DangerousChangeType,
| findBreakingChanges,
| findDangerousChanges,
|} from './findBreakingChanges';
|// Report all deprecated usage within a GraphQL document.
|export { findDeprecatedUsages } from './findDeprecatedUsages';
|export type {
| IntrospectionDirective,
| IntrospectionEnumType,
| IntrospectionEnumValue,
| IntrospectionField,
| IntrospectionInputObjectType,
| IntrospectionInputType,
| IntrospectionInputTypeRef,
| IntrospectionInputValue,
| IntrospectionInterfaceType,
| IntrospectionListTypeRef,
| IntrospectionNamedTypeRef,
| IntrospectionNonNullTypeRef,
| IntrospectionObjectType,
| IntrospectionOptions,
| IntrospectionOutputType,
| IntrospectionOutputTypeRef,
| IntrospectionQuery,
| IntrospectionScalarType,
| IntrospectionSchema,
| IntrospectionType,
| IntrospectionTypeRef,
| IntrospectionUnionType,
|} from './getIntrospectionQuery';
|export { getIntrospectionQuery } from './getIntrospectionQuery';
|// Gets the target Operation from a Document.
|export { getOperationAST } from './getOperationAST';
|// Gets the Type for the target Operation AST.
|export { getOperationRootType } from './getOperationRootType';
|// Convert a GraphQLSchema to an IntrospectionQuery.
|export { introspectionFromSchema } from './introspectionFromSchema';
|// Sort a GraphQLSchema.
|export { lexicographicSortSchema } from './lexicographicSortSchema';
|// Print a GraphQLSchema to GraphQL Schema language.
|export {
| printIntrospectionSchema,
| printSchema,
| printType,
|} from './printSchema';
|// Separates an AST into an AST per Operation.
|export { separateOperations } from './separateOperations';
|// Strips characters that are not significant to the validity or execution
|// of a GraphQL document.
|export { stripIgnoredCharacters } from './stripIgnoredCharacters';
|// Comparators for types
|export {
| doTypesOverlap,
| isEqualType,
| isTypeSubTypeOf,
|} from './typeComparators';
|// Create a GraphQLType from a GraphQL language AST.
|export { typeFromAST } from './typeFromAST';
|// A helper to use within recursive-descent visitors which need to be aware of
|// the GraphQL type system.
|export { TypeInfo, visitWithTypeInfo } from './TypeInfo';
|// Create a JavaScript value from a GraphQL language AST with a type.
|export { valueFromAST } from './valueFromAST';
|// Create a JavaScript value from a GraphQL language AST without a type.
|export { valueFromASTUntyped } from './valueFromASTUntyped';
`);
},
errors: 1,
}, All the blank lines are gone (because that’s how the sorting algorithm currently works – all blank lines are removed, because it’s unclear where they should go after sorting). Need to think about this one. |
Perhaps if the export is preceded by a comment, and the comment is preceded by a newline, the preceding newline should be preserved as well. Alternatively sorting exports could be a separate rule. This would make the feature opt-in, meaning people can choose not to enable it. |
I experimented locally and came up with:
Seems to work nicely so far. |
Are blank lines surrounding single line exports removed? Considering the following situation: export {
QuiteLongVariableName,
AnotherLongVariableName,
} from './longVariablesModule'
export { whatever } from 'module' Now one export is removed. export {
QuiteLongVariableName,
} from './longVariablesModule'
export { whatever } from 'module' Apply Prettier: export { QuiteLongVariableName } from './longVariablesModule'
export { whatever } from 'module' Now I would expect this plugin to remove the blank line. export { QuiteLongVariableName } from './longVariablesModule'
export { whatever } from 'module' |
Yes, that’s how it works. Another idea I’m going to explore is sorting chunks imports separately from chunks of re-exports and not touching other exports at all except sorting specifiers in |
I decided to take a break from this during the summer. I'll get back to it this fall. |