diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml index 5550acf162..8abcd8ab8d 100644 --- a/.github/workflows/test-pr.yaml +++ b/.github/workflows/test-pr.yaml @@ -49,7 +49,7 @@ jobs: - json-ts-csharp - dart,schema-dart # - swift,schema-swift # pgp issue - - javascript-prop-types + - javascript-prop-types,schema-javascript-prop-types - ruby - php,schema-php - scala3,schema-scala3 diff --git a/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts b/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts index 67b37614d4..d4859b792a 100644 --- a/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts +++ b/packages/quicktype-core/src/language/JavaScriptPropTypes/JavaScriptPropTypesRenderer.ts @@ -20,6 +20,7 @@ import { type ArrayType, type ClassProperty, type ClassType, + type MapType, type ObjectType, PrimitiveType, type Type, @@ -27,6 +28,7 @@ import { import { directlyReachableSingleNamedType, matchType, + removeNullFromType, } from "../../Type/TypeUtils.js"; import { isES3IdentifierStart } from "../JavaScript/unicodeMaps.js"; import { legalizeName } from "../JavaScript/utils.js"; @@ -92,9 +94,16 @@ export class JavaScriptPropTypesRenderer extends ConvenienceRenderer { return super.makeNameForProperty(c, className, p, jsonName, undefined); } - private typeMapTypeFor(t: Type, required = true): Sourcelike { + private typeMapTypeFor(t: Type, required = false): Sourcelike { if (["class", "object", "enum"].includes(t.kind)) { - return ["_", this.nameForNamedType(t)]; + const validator: Sourcelike = [ + "_", + this.nameForNamedType(t), + required ? ".isRequired" : "", + ]; + return t.kind === "enum" + ? validator + : ["(...args) => ", validator, "(...args)"]; } const match = matchType( @@ -129,14 +138,15 @@ export class JavaScriptPropTypesRenderer extends ConvenienceRenderer { ); if (required) { - return [match]; + return [match, ".isRequired"]; } return match; } private typeMapTypeForProperty(p: ClassProperty): Sourcelike { - return this.typeMapTypeFor(p.type); + const [nullType] = removeNullFromType(p.type); + return this.typeMapTypeFor(p.type, !p.isOptional && nullType === null); } private importStatement( @@ -211,10 +221,10 @@ export class JavaScriptPropTypesRenderer extends ConvenienceRenderer { this.forEachEnumCase( enumType, "none", - (name: Name, _jsonName, _position) => { - options.push("'"); - options.push(name); - options.push("'"); + (_name: Name, jsonName, _position) => { + options.push('"'); + options.push(utf16StringEscape(jsonName)); + options.push('"'); options.push(", "); }, ); @@ -223,7 +233,7 @@ export class JavaScriptPropTypesRenderer extends ConvenienceRenderer { this.emitLine([ "const _", enumName, - " = PropTypes.oneOfType([", + " = PropTypes.oneOf([", ...options, "]);", ]); @@ -281,6 +291,13 @@ export class JavaScriptPropTypesRenderer extends ConvenienceRenderer { this.typeMapTypeFor((type as ArrayType).items), ")", ]); + } else if (type.kind === "map") { + this.ensureBlankLine(); + this.emitExport(name, [ + "PropTypes.objectOf(", + this.typeMapTypeFor((type as MapType).values), + ")", + ]); } else { this.ensureBlankLine(); this.emitExport(name, ["_", name]); diff --git a/test/fixtures.ts b/test/fixtures.ts index 84616537e7..fe247a12de 100644 --- a/test/fixtures.ts +++ b/test/fixtures.ts @@ -929,6 +929,23 @@ class JSONSchemaFixture extends LanguageFixture { } } +class JavaScriptPropTypesJSONSchemaFixture extends JSONSchemaFixture { + constructor() { + super(languages.JavaScriptPropTypesLanguage); + } + + getSamples(sources: string[]): { priority: Sample[]; others: Sample[] } { + return super.getSamples( + sources.length === 0 + ? [ + "test/inputs/schema/javascript-prop-types/javascript-prop-types-map.schema", + "test/inputs/schema/javascript-prop-types/javascript-prop-types-required.schema", + ] + : sources, + ); + } +} + // `leadingComments` is a quicktype-core API option, so the CLI fixture path // cannot exercise it. class LeadingCommentsGoFixture extends JSONSchemaFixture { @@ -1641,11 +1658,7 @@ class CommandSuccessfulLanguageFixture extends LanguageFixture { } const command = this.language.runCommand(filename); - const results = await execAsync(command); - - if (results.stdout.indexOf("Success") === -1) { - throw new Error(`Test failed:\n${results.stdout}`); - } + await execAsync(command); return 0; } @@ -1819,6 +1832,7 @@ export const allFixtures: Fixture[] = [ new JSONSchemaFixture(languages.TypeScriptZodLanguage), new JSONSchemaFixture(languages.FlowLanguage), new JSONSchemaFixture(languages.JavaScriptLanguage), + new JavaScriptPropTypesJSONSchemaFixture(), new JSONSchemaFixture(languages.KotlinLanguage), new JSONSchemaFixture( languages.KotlinJacksonLanguage, diff --git a/test/fixtures/javascript-prop-types/main.js b/test/fixtures/javascript-prop-types/main.js index a8795f3f90..27b6185c01 100644 --- a/test/fixtures/javascript-prop-types/main.js +++ b/test/fixtures/javascript-prop-types/main.js @@ -1,7 +1,6 @@ import { readFileSync } from "node:fs"; import { argv } from "node:process"; import PropTypes from "prop-types"; -import { TopLevel } from "./toplevel.js"; const sample = argv[2]; const json = readFileSync(sample); @@ -26,6 +25,7 @@ try { checkerWorks = errors.length > 0; errors.length = 0; + const { TopLevel } = await import("./toplevel.js"); PropTypes.resetWarningCache(); PropTypes.checkPropTypes({ obj: TopLevel }, { obj }, "prop", "MyComponent"); } finally { @@ -36,8 +36,10 @@ if (!checkerWorks) { console.log( "Failure: prop-types checks are disabled (NODE_ENV=production?)", ); + process.exitCode = 1; } else if (errors.length > 0) { console.log("Failure:", errors.join("\n")); + process.exitCode = 1; } else { - console.log("Success"); + console.log(JSON.stringify(obj)); } diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.1.fail.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.1.fail.json new file mode 100644 index 0000000000..3e1946bc9e --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.1.fail.json @@ -0,0 +1,3 @@ +{ + "first": 42 +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.json new file mode 100644 index 0000000000..0ee4c04574 --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.json @@ -0,0 +1,4 @@ +{ + "first": "value", + "second": "another value" +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.schema b/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.schema new file mode 100644 index 0000000000..dbd1f4b0f7 --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-map.schema @@ -0,0 +1,4 @@ +{ + "type": "object", + "additionalProperties": { "type": "string" } +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.1.fail.no-defaults.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.1.fail.no-defaults.json new file mode 100644 index 0000000000..dfdc101409 --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.1.fail.no-defaults.json @@ -0,0 +1,11 @@ +{ + "disabled": false, + "options": { + "label": "shown" + }, + "tags": [ + "one", + "two" + ], + "kind": "primary choice" +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.2.fail.no-defaults.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.2.fail.no-defaults.json new file mode 100644 index 0000000000..2b7691209f --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.2.fail.no-defaults.json @@ -0,0 +1,13 @@ +{ + "anything": { + "accepted": true + }, + "options": { + "label": "shown" + }, + "tags": [ + "one", + "two" + ], + "kind": "primary choice" +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.3.fail.no-defaults.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.3.fail.no-defaults.json new file mode 100644 index 0000000000..2e2f92396a --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.3.fail.no-defaults.json @@ -0,0 +1,11 @@ +{ + "anything": { + "accepted": true + }, + "disabled": false, + "tags": [ + "one", + "two" + ], + "kind": "primary choice" +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.4.fail.no-defaults.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.4.fail.no-defaults.json new file mode 100644 index 0000000000..3ef42cf9c5 --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.4.fail.no-defaults.json @@ -0,0 +1,10 @@ +{ + "anything": { + "accepted": true + }, + "disabled": false, + "options": { + "label": "shown" + }, + "kind": "primary choice" +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.5.fail.no-defaults.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.5.fail.no-defaults.json new file mode 100644 index 0000000000..9537c59dd1 --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.5.fail.no-defaults.json @@ -0,0 +1,13 @@ +{ + "anything": { + "accepted": true + }, + "disabled": false, + "options": { + "label": "shown" + }, + "tags": [ + "one", + "two" + ] +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.6.fail.no-defaults.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.6.fail.no-defaults.json new file mode 100644 index 0000000000..160b01d73a --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.6.fail.no-defaults.json @@ -0,0 +1,12 @@ +{ + "anything": { + "accepted": true + }, + "disabled": false, + "options": {}, + "tags": [ + "one", + "two" + ], + "kind": "primary choice" +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.json b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.json new file mode 100644 index 0000000000..ebbf0cdbea --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.json @@ -0,0 +1,14 @@ +{ + "anything": { + "accepted": true + }, + "disabled": false, + "options": { + "label": "shown" + }, + "tags": [ + "one", + "two" + ], + "kind": "primary choice" +} diff --git a/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.schema b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.schema new file mode 100644 index 0000000000..c84f0f2dc7 --- /dev/null +++ b/test/inputs/schema/javascript-prop-types/javascript-prop-types-required.schema @@ -0,0 +1,24 @@ +{ + "type": "object", + "required": ["anything", "disabled", "options", "tags", "kind"], + "properties": { + "anything": {}, + "disabled": { "type": "boolean" }, + "options": { + "type": "object", + "required": ["label"], + "properties": { + "label": { "type": "string" } + } + }, + "tags": { + "type": "array", + "items": { "type": "string" } + }, + "kind": { + "type": "string", + "enum": ["primary choice", "secondary"] + }, + "optional": { "type": "number" } + } +}