diff --git a/codegen-test/.gitignore b/codegen-test/.gitignore new file mode 100644 index 000000000..063134a24 --- /dev/null +++ b/codegen-test/.gitignore @@ -0,0 +1 @@ +types.ts diff --git a/codegen-test/fixtures/geojson.ts b/codegen-test/fixtures/geojson.ts new file mode 100644 index 000000000..05c708861 --- /dev/null +++ b/codegen-test/fixtures/geojson.ts @@ -0,0 +1,121 @@ +// Type-level test for the generated GeoJSON codegen type. +// +// Typechecking `types.ts` on its own only proves the emitted type is *valid* +// TypeScript. The GeoJSON precedence bug produced a valid-but-wrong type +// (`Position | Position[]` without parentheses before `[]`), so `tsc` accepted +// it. Assigning real values is what exercises the type: an arrayed geometry's +// coordinates stop being assignable the moment the union isn't grouped. +// +// `input` and `output` are the same union for this scalar; testing one covers both. +import type { Scalars } from '../types'; + +type GeoJSON = Scalars['GeoJSON']['input']; + +// Valid — every geometry from the scalar's own validExamples must be assignable. +const point: GeoJSON = { + type: 'Point', + coordinates: [100.0, 0.0], +}; + +const multiPoint: GeoJSON = { + type: 'MultiPoint', + coordinates: [ + [100.0, 0.0], + [101.0, 1.0], + ], +}; + +const lineString: GeoJSON = { + type: 'LineString', + coordinates: [ + [100.0, 0.0], + [101.0, 1.0], + ], +}; + +const multiLineString: GeoJSON = { + type: 'MultiLineString', + coordinates: [ + [ + [100.0, 0.0], + [101.0, 1.0], + ], + [ + [102.0, 2.0], + [103.0, 3.0], + ], + ], +}; + +const polygon: GeoJSON = { + type: 'Polygon', + coordinates: [ + [ + [100.0, 0.0], + [101.0, 0.0], + [101.0, 1.0], + [100.0, 1.0], + [100.0, 0.0], + ], + ], +}; + +const multiPolygon: GeoJSON = { + type: 'MultiPolygon', + coordinates: [ + [ + [ + [102.0, 2.0], + [103.0, 2.0], + [103.0, 3.0], + [102.0, 3.0], + [102.0, 2.0], + ], + ], + ], +}; + +const geometryCollection: GeoJSON = { + type: 'GeometryCollection', + geometries: [{ type: 'Point', coordinates: [100.0, 0.0] }], +}; + +const feature: GeoJSON = { + type: 'Feature', + geometry: { type: 'Point', coordinates: [100.0, 0.0] }, + properties: { name: 'Test Point' }, +}; + +const featureCollection: GeoJSON = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { type: 'Point', coordinates: [100.0, 0.0] }, + properties: { name: 'Test Point' }, + }, + ], +}; + +// Invalid — a single Position is not an array of Positions. The unparenthesized +// bug wrongly accepts this; then the directive is unused and tsc fails, catching +// the bug from the "accepts too much" side and guarding against widening to `any`. +const barePosition: GeoJSON = { + type: 'MultiPoint', + // @ts-expect-error coordinates must be an array of Positions, not one Position + coordinates: [100.0, 0.0], +}; + +// Reference every binding so unused-local checks stay quiet. +void [ + point, + multiPoint, + lineString, + multiLineString, + polygon, + multiPolygon, + geometryCollection, + feature, + featureCollection, + barePosition, +]; diff --git a/codegen-test/package.json b/codegen-test/package.json index 77a9f2301..b7b8cf0e4 100644 --- a/codegen-test/package.json +++ b/codegen-test/package.json @@ -1,7 +1,15 @@ { "name": "codegen-scalars-test", + "scripts": { + "codegen": "graphql-codegen --config codegen.yml", + "test": "yarn codegen && tsc --noEmit -p tsconfig.json" + }, "dependencies": { "@graphql-codegen/cli": "6.2.1", "@graphql-codegen/typescript": "5.0.9" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.5.0" } } diff --git a/codegen-test/tsconfig.json b/codegen-test/tsconfig.json index ec02ecde1..bfaa4c53b 100644 --- a/codegen-test/tsconfig.json +++ b/codegen-test/tsconfig.json @@ -1,8 +1,15 @@ { "compilerOptions": { + "noEmit": true, + "skipLibCheck": true, + "strict": false, + "moduleResolution": "node", + "lib": ["ESNext", "DOM"], + "types": ["node"], + "baseUrl": ".", "paths": { "graphql-scalars": ["../src/index.ts"] } }, - "files": ["./types.ts"] + "include": ["./types.ts", "./fixtures/**/*.ts"] } diff --git a/codegen-test/types.ts b/codegen-test/types.ts deleted file mode 100644 index dd450b754..000000000 --- a/codegen-test/types.ts +++ /dev/null @@ -1,227 +0,0 @@ -export type Maybe = T | null; -export type Exact = { - [K in keyof T]: T[K]; -}; -export type MakeOptional = Omit & { - [SubKey in K]?: Maybe; -}; -export type MakeMaybe = Omit & { - [SubKey in K]: Maybe; -}; -/** All built-in and custom scalars, mapped to their actual values */ -export type Scalars = { - ID: string; - String: string; - Boolean: boolean; - Int: number; - Float: number; - /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */ - BigInt: import('graphql-scalars').BigIntTSType; - /** The `Byte` scalar type represents byte value as a Buffer */ - Byte: import('graphql-scalars').ByteTSType; - /** A field whose value is a Currency: https://en.wikipedia.org/wiki/ISO_4217. */ - Currency: import('graphql-scalars').CurrencyTSType; - /** A date string, such as 2007-12-03, compliant with the `full-date` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ - Date: import('graphql-scalars').DateTSType; - /** A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ - DateTime: import('graphql-scalars').DateTimeTSType; - /** - * - * A string representing a duration conforming to the ISO8601 standard, - * such as: P1W1DT13H23M34S - * P is the duration designator (for period) placed at the start of the duration representation. - * Y is the year designator that follows the value for the number of years. - * M is the month designator that follows the value for the number of months. - * W is the week designator that follows the value for the number of weeks. - * D is the day designator that follows the value for the number of days. - * T is the time designator that precedes the time components of the representation. - * H is the hour designator that follows the value for the number of hours. - * M is the minute designator that follows the value for the number of minutes. - * S is the second designator that follows the value for the number of seconds. - * - * Note the time designator, T, that precedes the time value. - * - * Matches moment.js, Luxon and DateFns implementations - * ,/. is valid for decimal places and +/- is a valid prefix - * - */ - Duration: import('graphql-scalars').DurationTSType; - /** A field whose value conforms to the standard internet email address format as specified in RFC822: https://www.w3.org/Protocols/rfc822/ */ - EmailAddress: import('graphql-scalars').EmailAddressTSType; - /** A field whose value is a generic Universally Unique Identifier: https://en.wikipedia.org/wiki/Universally_unique_identifier. */ - GUID: `${string}-${string}-${string}-${string}-${string}`; - /** A field whose value is a CSS HSL color: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#hsl()_and_hsla(). */ - HSL: string; - /** A field whose value is a CSS HSLA color: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#hsl()_and_hsla(). */ - HSLA: string; - /** A field whose value is a hex color code: https://en.wikipedia.org/wiki/Web_colors. */ - HexColorCode: `#${string}`; - /** A field whose value is a hexadecimal: https://en.wikipedia.org/wiki/Hexadecimal. */ - Hexadecimal: string; - /** A field whose value is an International Bank Account Number (IBAN): https://en.wikipedia.org/wiki/International_Bank_Account_Number. */ - IBAN: string; - /** A field whose value is a IPv4 address: https://en.wikipedia.org/wiki/IPv4. */ - IPv4: - | `${number}.${number}.${number}.${number}` - | `${number}.${number}.${number}.${number}/${number}`; - /** A field whose value is a IPv6 address: https://en.wikipedia.org/wiki/IPv6. */ - IPv6: `${string}:${string}:${string}:${string}:${string}:${string}:${string}:${string}`; - /** A field whose value is a ISBN-10 or ISBN-13 number: https://en.wikipedia.org/wiki/International_Standard_Book_Number. */ - ISBN: string; - /** - * - * A string representing a duration conforming to the ISO8601 standard, - * such as: P1W1DT13H23M34S - * P is the duration designator (for period) placed at the start of the duration representation. - * Y is the year designator that follows the value for the number of years. - * M is the month designator that follows the value for the number of months. - * W is the week designator that follows the value for the number of weeks. - * D is the day designator that follows the value for the number of days. - * T is the time designator that precedes the time components of the representation. - * H is the hour designator that follows the value for the number of hours. - * M is the minute designator that follows the value for the number of minutes. - * S is the second designator that follows the value for the number of seconds. - * - * Note the time designator, T, that precedes the time value. - * - * Matches moment.js, Luxon and DateFns implementations - * ,/. is valid for decimal places and +/- is a valid prefix - * - */ - ISO8601Duration: import('graphql-scalars').DurationTSType; - /** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ - JSON: any; - /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */ - JSONObject: Record; - /** A field whose value is a JSON Web Token (JWT): https://jwt.io/introduction. */ - JWT: string; - /** A field whose value is a valid decimal degrees latitude number (53.471): https://en.wikipedia.org/wiki/Latitude */ - Latitude: string; - /** A local date string (i.e., with no associated timezone) in `YYYY-MM-DD` format, e.g. `2020-01-01`. */ - LocalDate: `${number}-${number}-${number}`; - /** A local time string (i.e., with no associated timezone) in 24-hr `HH:mm[:ss[.SSS]]` format, e.g. `14:25` or `14:25:06` or `14:25:06.123`. This scalar is very similar to the `LocalTime`, with the only difference being that `LocalEndTime` also allows `24:00` as a valid value to indicate midnight of the following day. This is useful when using the scalar to represent the exclusive upper bound of a time block. */ - LocalEndTime: - | `${number}:${number}` - | `${number}:${number}:${number}` - | `${number}:${number}:${number}.${number}`; - /** A local time string (i.e., with no associated timezone) in 24-hr `HH:mm[:ss[.SSS]]` format, e.g. `14:25` or `14:25:06` or `14:25:06.123`. */ - LocalTime: - | `${number}:${number}` - | `${number}:${number}:${number}` - | `${number}:${number}:${number}.${number}`; - /** The `BigInt` scalar type represents non-fractional signed whole numeric values. */ - Long: import('graphql-scalars').BigIntTSType; - /** A field whose value is a valid decimal degrees longitude number (53.471): https://en.wikipedia.org/wiki/Longitude */ - Longitude: string | number; - /** A field whose value is a IEEE 802 48-bit MAC address: https://en.wikipedia.org/wiki/MAC_address. */ - MAC: - | `${string}:${string}:${string}:${string}:${string}:${string}` - | `${string}-${string}-${string}-${string}-${string}-${string}` - | `${string}.${string}.${string}`; - /** Floats that will have a value less than 0. */ - NegativeFloat: number; - /** Integers that will have a value less than 0. */ - NegativeInt: number; - /** A string that cannot be passed as an empty value */ - NonEmptyString: string; - /** Floats that will have a value of 0 or more. */ - NonNegativeFloat: number; - /** Integers that will have a value of 0 or more. */ - NonNegativeInt: number; - /** Floats that will have a value of 0 or less. */ - NonPositiveFloat: number; - /** Integers that will have a value of 0 or less. */ - NonPositiveInt: number; - /** A field whose value conforms with the standard mongodb object ID as described here: https://docs.mongodb.com/manual/reference/method/ObjectId/#ObjectId. Example: 5e5677d71bdc2ae76344968c */ - ObjectID: string; - /** A field whose value conforms to the standard E.164 format as specified in: https://en.wikipedia.org/wiki/E.164. Basically this is +17895551234. */ - PhoneNumber: `+${number}`; - /** A field whose value is a valid TCP port within the range of 0 to 65535: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#TCP_ports */ - Port: string | number; - /** Floats that will have a value greater than 0. */ - PositiveFloat: number; - /** Integers that will have a value greater than 0. */ - PositiveInt: number; - /** A field whose value conforms to the standard postal code formats for United States, United Kingdom, Germany, Canada, France, Italy, Australia, Netherlands, Spain, Denmark, Sweden, Belgium, India, Austria, Portugal, Switzerland or Luxembourg. */ - PostalCode: string; - /** A field whose value is a CSS RGB color: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#rgb()_and_rgba(). */ - RGB: `rgb(${number}, ${number}, ${number})`; - /** A field whose value is a CSS RGBA color: https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#rgb()_and_rgba(). */ - RGBA: `rgba(${number}, ${number}, ${number})`; - /** The `SafeInt` scalar type represents non-fractional signed whole numeric values that are considered safe as defined by the ECMAScript specification. */ - SafeInt: number; - /** A time string at UTC, such as 10:15:30Z, compliant with the `full-time` format outlined in section 5.6 of the RFC 3339profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar. */ - Time: import('graphql-scalars').TimeTSType; - /** The javascript `Date` as integer. Type represents date and time as number of milliseconds from start of UNIX epoch. */ - Timestamp: Date | string | number; - /** A field whose value conforms to the standard URL format as specified in RFC3986: https://www.ietf.org/rfc/rfc3986.txt. */ - URL: URL | `${string}://${string}`; - /** A currency string, such as $21.25 */ - USCurrency: string; - /** A field whose value is a generic Universally Unique Identifier: https://en.wikipedia.org/wiki/Universally_unique_identifier. */ - UUID: `${string}-${string}-${string}-${string}-${string}`; - /** Floats that will have a value of 0 or more. */ - UnsignedFloat: number; - /** Integers that will have a value of 0 or more. */ - UnsignedInt: number; - /** A field whose value is a UTC Offset: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones */ - UtcOffset: `-${number}:${number}` | `+${number}:${number}`; - /** Represents NULL values */ - Void: void; -}; - -export type Query = { - __typename?: 'Query'; - bigint?: Maybe; - byte?: Maybe; - currency?: Maybe; - date?: Maybe; - datetime?: Maybe; - duration?: Maybe; - emailaddress?: Maybe; - guid?: Maybe; - hexadecimal?: Maybe; - hexcolorcode?: Maybe; - hsl?: Maybe; - hsla?: Maybe; - iban?: Maybe; - ipv4?: Maybe; - ipv6?: Maybe; - isbn?: Maybe; - iso8601duration?: Maybe; - json?: Maybe; - jsonobject?: Maybe; - jwt?: Maybe; - latitude?: Maybe; - localdate?: Maybe; - localendtime?: Maybe; - localtime?: Maybe; - long?: Maybe; - longitude?: Maybe; - mac?: Maybe; - negativefloat?: Maybe; - negativeint?: Maybe; - nonemptystring?: Maybe; - nonnegativefloat?: Maybe; - nonnegativeint?: Maybe; - nonpositivefloat?: Maybe; - nonpositiveint?: Maybe; - objectid?: Maybe; - phonenumber?: Maybe; - port?: Maybe; - positivefloat?: Maybe; - positiveint?: Maybe; - postalcode?: Maybe; - rgb?: Maybe; - rgba?: Maybe; - safeint?: Maybe; - time?: Maybe; - timestamp?: Maybe; - unsignedfloat?: Maybe; - unsignedint?: Maybe; - url?: Maybe; - uscurrency?: Maybe; - utcoffset?: Maybe; - uuid?: Maybe; - void?: Maybe; -}; diff --git a/codegen-test/yarn.lock b/codegen-test/yarn.lock index 0e3ca3a6b..147ada198 100644 --- a/codegen-test/yarn.lock +++ b/codegen-test/yarn.lock @@ -841,6 +841,13 @@ dependencies: undici-types "~7.18.0" +"@types/node@^22.0.0": + version "22.20.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.20.1.tgz#84e7cdf63cdaa20c134aa317ccc901aa21e16f0e" + integrity sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q== + dependencies: + undici-types "~6.21.0" + "@types/ws@^8.0.0": version "8.18.1" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.18.1.tgz#48464e4bf2ddfd17db13d845467f6070ffea4aa9" @@ -1969,11 +1976,21 @@ tslib@~2.6.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +typescript@^5.5.0: + version "5.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f" + integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== + unc-path-regex@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa" integrity sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg== +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + undici-types@~7.18.0: version "7.18.2" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.18.2.tgz#29357a89e7b7ca4aef3bf0fd3fd0cd73884229e9" diff --git a/package.json b/package.json index eaacae684..d5f076d34 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "bundlesize": "yarn build && cd bundle-test/ && yarn && yarn test", "ci:lint": "cross-env \"ESLINT_USE_FLAT_CONFIG=false\" eslint --ext .ts \"./src/**/*.ts\" --output-file eslint_report.json --format json", "clean": "rm -rf dist", + "codegen-test": "yarn build && cd codegen-test/ && yarn && yarn test", "deploy:website": "cd website && yarn deploy", "lint": "cross-env \"ESLINT_USE_FLAT_CONFIG=false\" eslint --ext .ts \"./src/**/*.ts\"", "lint:prettier": "prettier --ignore-path .gitignore --ignore-path .prettierignore --cache --check .", @@ -45,7 +46,7 @@ "prettier": "yarn lint:prettier --write", "release": "changeset publish", "setup-scalar": "node ./scripts/setup-scalar.js", - "test": "jest --forceExit --no-watchman && yarn bundlesize" + "test": "jest --forceExit --no-watchman && yarn bundlesize && yarn codegen-test" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" diff --git a/src/scalars/GeoJSON/codegenScalarType.ts b/src/scalars/GeoJSON/codegenScalarType.ts index d46221c40..63c40a96c 100644 --- a/src/scalars/GeoJSON/codegenScalarType.ts +++ b/src/scalars/GeoJSON/codegenScalarType.ts @@ -1,5 +1,7 @@ -// Helper function to generate Position type -const generatePositionType = () => `[number, number] | [number, number, number]`; +// Helper function to generate Position type. +// Parenthesized so that array usages (`${generatePositionType()}[]`) apply `[]` +// to the whole union rather than only the second member. +const generatePositionType = () => `([number, number] | [number, number, number])`; // Helper function to generate BBox type const generateBBoxType = () =>