diff --git a/src/parser.ts b/src/parser.ts index 625152e..c2aad28 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -394,20 +394,15 @@ export function parseFromProgram( throw new Error('No types found'); } - // Typechecker only gives the type "any" if it's present in a union - // This means the type of "a" in {a?:any} isn't "any | undefined" - // So instead we check for the questionmark to detect optional types - let parsedType: t.Node | undefined = undefined; - if ( - (type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) && - declaration && - ts.isPropertySignature(declaration) - ) { - parsedType = declaration.questionToken - ? t.unionNode([t.undefinedNode(), t.anyNode()]) - : t.anyNode(); - } else { - parsedType = checkType(type, typeStack, symbol.getName()); + let parsedType = checkType(type, typeStack, symbol.getName()); + + // In strict mode, the type of "a" in {a?: type} is "type | undefined", + // unless the type is "any", in which case TypeChecker only gives "any" + // Outside of strict mode, the type of "a" in {a?: type} is simply "type". + // To cover both of these cases we instead check for the questionmark + // to detect optional types + if (declaration && ts.isPropertySignature(declaration) && declaration.questionToken) { + parsedType = t.unionNode([t.undefinedNode(), parsedType]); } return t.propTypeNode( @@ -440,7 +435,7 @@ export function parseFromProgram( return t.elementNode('elementType'); } case 'React.ReactNode': { - return t.unionNode([t.elementNode('node'), t.undefinedNode()]); + return t.unionNode([t.undefinedNode(), t.elementNode('node')]); } case 'Date': case 'React.Component': { diff --git a/test/index.test.ts b/test/index.test.ts index 65c9375..9c7c4dc 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -10,99 +10,108 @@ const prettierConfig = prettier.resolveConfig.sync(path.join(__dirname, '../.pre const testCases = glob.sync('**/input.{d.ts,ts,tsx}', { absolute: true, cwd: __dirname }); -// Create program for all files to speed up tests -const program = ttp.createProgram( - testCases, - ttp.loadConfig(path.resolve(__dirname, '../tsconfig.json')) -); +const tsModesConfigs = { + 'default Typescript config': '../tsconfig.json', + 'strict mode disabled': './tsconfig.nostrict.json', +}; -for (const testCase of testCases) { - const dirname = path.dirname(testCase); - const testName = dirname.substr(__dirname.length + 1); - const astPath = path.join(dirname, 'output.json'); - const outputPath = path.join(dirname, 'output.js'); - const optionsPath = path.join(dirname, 'options.ts'); - const inputJS = path.join(dirname, 'input.js'); +for (const [mode, configRelativePath] of Object.entries(tsModesConfigs)) { + describe(`given ${mode}`, () => { + // Create program for all files to speed up tests + const program = ttp.createProgram( + testCases, + ttp.loadConfig(path.resolve(__dirname, configRelativePath)) + ); - it(testName, () => { - const options: TestOptions = fs.existsSync(optionsPath) ? require(optionsPath).default : {}; + for (const testCase of testCases) { + const dirname = path.dirname(testCase); + const testName = dirname.substr(__dirname.length + 1); + const astPath = path.join(dirname, 'output.json'); + const outputPath = path.join(dirname, 'output.js'); + const optionsPath = path.join(dirname, 'options.ts'); + const inputJS = path.join(dirname, 'input.js'); - const ast = ttp.parseFromProgram(testCase, program, options.parser); + it(testName, () => { + const options: TestOptions = fs.existsSync(optionsPath) ? require(optionsPath).default : {}; - //#region Check AST matches - // propsFilename will be different depending on where the project is on disk - // Manually check that it's correct and then delete it - const newAST = ttp.programNode( - ast.body.map((component) => { - expect(component.propsFilename).toBe(testCase); - return { ...component, propsFilename: undefined }; - }) - ); + const ast = ttp.parseFromProgram(testCase, program, options.parser); - if (fs.existsSync(astPath)) { - expect(newAST).toMatchObject(JSON.parse(fs.readFileSync(astPath, 'utf8'))); - } else { - fs.writeFileSync( - astPath, - prettier.format( - JSON.stringify(newAST, (key, value) => { - // These are TypeScript internals that change depending on the number of symbols created during test - if (key === '$$id') { - return undefined; - } - return value; - }), - { - ...prettierConfig, - filepath: astPath, - } - ) - ); - } - //#endregion + //#region Check AST matches + // propsFilename will be different depending on where the project is on disk + // Manually check that it's correct and then delete it + const newAST = ttp.programNode( + ast.body.map((component) => { + expect(component.propsFilename).toBe(testCase); + return { ...component, propsFilename: undefined }; + }) + ); - let inputSource = null; - if (testCase.endsWith('.d.ts')) { - try { - inputSource = fs.readFileSync(inputJS, 'utf8'); - } catch (error) {} - } else { - inputSource = ttp.ts.transpileModule(fs.readFileSync(testCase, 'utf8'), { - compilerOptions: { - target: ttp.ts.ScriptTarget.ESNext, - jsx: ttp.ts.JsxEmit.Preserve, - }, - }).outputText; - } + if (fs.existsSync(astPath)) { + expect(newAST).toMatchObject(JSON.parse(fs.readFileSync(astPath, 'utf8'))); + } else { + fs.writeFileSync( + astPath, + prettier.format( + JSON.stringify(newAST, (key, value) => { + // These are TypeScript internals that change depending on the number of symbols created during test + if (key === '$$id') { + return undefined; + } + return value; + }), + { + ...prettierConfig, + filepath: astPath, + } + ) + ); + } + //#endregion - let result = ''; - // For d.ts files we just generate the AST - if (!inputSource) { - result = ttp.generate(ast, options.generator); - } - // For .tsx? files we transpile them and inject the proptypes - else { - const injected = ttp.inject(ast, inputSource, options.injector); - if (!injected) { - throw new Error('Injection failed'); - } + let inputSource: string | null = null; + if (testCase.endsWith('.d.ts')) { + try { + inputSource = fs.readFileSync(inputJS, 'utf8'); + } catch (error) {} + } else { + inputSource = ttp.ts.transpileModule(fs.readFileSync(testCase, 'utf8'), { + compilerOptions: { + target: ttp.ts.ScriptTarget.ESNext, + jsx: ttp.ts.JsxEmit.Preserve, + }, + }).outputText; + } - result = injected; - } + let result = ''; + // For d.ts files we just generate the AST + if (!inputSource) { + result = ttp.generate(ast, options.generator); + } + // For .tsx? files we transpile them and inject the proptypes + else { + const injected = ttp.inject(ast, inputSource, options.injector); + if (!injected) { + throw new Error('Injection failed'); + } + + result = injected; + } - //#region Check generated and/or injected proptypes - const propTypes = prettier.format(result, { - ...prettierConfig, - filepath: outputPath, - }); + //#region Check generated and/or injected proptypes + const propTypes = prettier.format(result, { + ...prettierConfig, + filepath: outputPath, + }); - if (fs.existsSync(outputPath)) { - expect(propTypes.replace(/\r?\n/g, '\n')).toMatch( - fs.readFileSync(outputPath, 'utf8').replace(/\r?\n/g, '\n') - ); - } else { - fs.writeFileSync(outputPath, propTypes); + if (fs.existsSync(outputPath)) { + expect(propTypes.replace(/\r?\n/g, '\n')).toMatch( + fs.readFileSync(outputPath, 'utf8').replace(/\r?\n/g, '\n') + ); + } else { + fs.writeFileSync(outputPath, propTypes); + } + //#endregion + }); } - //#endregion }); } diff --git a/test/injector/should-include-filename-based/output.json b/test/injector/should-include-filename-based/output.json index bec47bd..dde55a5 100644 --- a/test/injector/should-include-filename-based/output.json +++ b/test/injector/should-include-filename-based/output.json @@ -1079,7 +1079,7 @@ "name": "children", "propType": { "type": "UnionNode", - "types": [{ "type": "ElementNode", "elementType": "node" }, { "type": "UndefinedNode" }] + "types": [{ "type": "UndefinedNode" }, { "type": "ElementNode", "elementType": "node" }] }, "filenames": {} }, @@ -2556,7 +2556,7 @@ "name": "children", "propType": { "type": "UnionNode", - "types": [{ "type": "ElementNode", "elementType": "node" }, { "type": "UndefinedNode" }] + "types": [{ "type": "UndefinedNode" }, { "type": "ElementNode", "elementType": "node" }] }, "filenames": {} } diff --git a/test/reconcile-prop-types/output.json b/test/reconcile-prop-types/output.json index e4d7144..cab4139 100644 --- a/test/reconcile-prop-types/output.json +++ b/test/reconcile-prop-types/output.json @@ -10,7 +10,7 @@ "name": "children", "propType": { "type": "UnionNode", - "types": [{ "type": "ElementNode", "elementType": "node" }, { "type": "UndefinedNode" }] + "types": [{ "type": "UndefinedNode" }, { "type": "ElementNode", "elementType": "node" }] } } ] diff --git a/test/tsconfig.nostrict.json b/test/tsconfig.nostrict.json new file mode 100644 index 0000000..c1f3188 --- /dev/null +++ b/test/tsconfig.nostrict.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "strict": false + } +}