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

feat(generators): add typescript support #532

Merged
merged 2 commits into from
Jul 13, 2018

Conversation

dhruvdutt
Copy link
Member

@dhruvdutt dhruvdutt commented Jun 27, 2018

What kind of change does this PR introduce?

Did you add tests for your changes?

If relevant, did you update the documentation?

Summary

Does this PR introduce a breaking change?

Other information

Not ready for review yet.

Copy link
Member

@evenstensberg evenstensberg left a comment

Choose a reason for hiding this comment

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

You'll need to go over this again and swap any with specific types. Did a first review but it's too much of any here to comment them all. Do a console.log(typeof thisVal) if you're insecure about the type of the values you're adding.

const webpackDevServerSchema = require("webpack-dev-server/lib/optionsSchema.json");
const PROP_TYPES = require("@webpack-cli/utils/prop-types");
import glob from "glob-all";
import inquirerAutoComplete from "inquirer-autocomplete-prompt";
Copy link
Member

Choose a reason for hiding this comment

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

import autoComplete

PROPS.filter(food =>
food.toLowerCase().includes(input.toLowerCase())
)
PROPS.filter((food: string) =>
Copy link
Member

Choose a reason for hiding this comment

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

Why is this named food? ^_^

let manualOrListInput = action =>
Input("actionAnswer", `What do you want to add to ${action}?`);
public prompting() {
const done: any = this.async();
Copy link
Member

Choose a reason for hiding this comment

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

This is a promise that resolves to a boolean if I remember right

Copy link
Member Author

Choose a reason for hiding this comment

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

You mean something like?

const done: Promise<boolean>

Also, if it's a done is a promise, the compiler would fail all calls like done(). I think done is a function. Something like this should work:

const done: () => void | boolean

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

If the asynchronous API you’re relying upon doesn’t support promises, then you can rely on the legacy this.async() way. Calling this.async() will return a function to call once the task is done.

It's not a promise. It could be something like this:

const done: () => void | boolean


// first index indicates if it has a deep prop, 2nd indicates what kind of
let isDeepProp = [false, false];
const isDeepProp: any[] = [false, false];
Copy link
Member

Choose a reason for hiding this comment

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

a bit naive to use any here when you can see the type

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. I tried boolean[] and Array<object | boolean> but both won't work because we also assign the values as boolean, string, object, array in different scopes. Some of the examples:

isDeepProp[1] = answerToAction.actionAnswer;

if (isDeepProp[2].length === 0) {

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

const isDeepProp: [boolean, string | boolean | object | object[]]

^This is a valid type declaration which allows to control specifically which element should be of which type. Even after declaring the second element to be one of the boolean, string, object or array of objects, the compiler fails. I think the complex reassignment logic we're doing inside the generator is making the compiler's type prediction to fail.

TS compiler works for simple logic like doing typeof var === "string" and then using it as string, the compiler infers it to be a string even if the type was string | boolean ...other types.

Copy link
Member

Choose a reason for hiding this comment

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

maybe add a interface for this if it's possible

if (action === "resolveLoader") {
action = "resolve";
}
const webpackSchemaProp = webpackSchema.definitions[action];
const webpackSchemaProp: any = webpackSchema.definitions[action];
Copy link
Member

Choose a reason for hiding this comment

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

avoid using any here, just check the prop instead

emit(event: string, ...args: any[]): boolean;
listenerCount(type: string): number;

async(): any;
Copy link
Member

Choose a reason for hiding this comment

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

async is probably a promise

prompt(opt: IPromptOptions | IPromptOptions[], callback: (answers: any) => void): void;
prompt<T>(opt: IPromptOptions | IPromptOptions[]): PromiseLike<T>;
log(message: string) : void;
npmInstall(packages: string[], options?: any, cb?: Function): any;
Copy link
Member

Choose a reason for hiding this comment

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

this is not any


appname: string;
gruntfile: IGruntFileStatic;
options: { [key: string]: any };
Copy link
Member

Choose a reason for hiding this comment

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

not any here

choices?: any[] | ((answers: Object) => any);
default?: string | number | string[] | number[] | ((answers: Object) => (string | number | string[] | number[]));
validate?: ((input: any) => boolean | string);
filter?: ((input: any) => any);
Copy link
Member

Choose a reason for hiding this comment

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

insecure about the use of any here

registerTask(name:string, tasks:any):void;
insertVariable(name:string, value:any):void;
prependJavaScript(code:string):void;
}
Copy link
Member

Choose a reason for hiding this comment

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

these any values should be more type specific

@ematipico
Copy link
Contributor

Please update the template with the relevant information and link this PR to the original issue

@webpack-bot
Copy link

@dhruvdutt Thanks for your update.

I labeled the Pull Request so reviewers will review it again.

@ev1stensberg Please review the new changes.

Copy link
Member

@evenstensberg evenstensberg left a comment

Choose a reason for hiding this comment

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

Could you rebase and I'll have another look?

@dhruvdutt dhruvdutt force-pushed the typescript-generators branch from 97c62f7 to 4561b57 Compare June 30, 2018 13:46
@dhruvdutt dhruvdutt changed the title [WIP] - feat(generators): add typescript support feat(generators): add typescript support Jun 30, 2018
@dhruvdutt dhruvdutt force-pushed the typescript-generators branch from 4561b57 to 5ac0f2b Compare June 30, 2018 18:51
configPath = null;
// end the generator stating webpack config not found or to specify the config
}
this.webpackOptions = require(configPath);
Copy link
Member

Choose a reason for hiding this comment

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

you'll need to do a json read on this, require's will try to resolve dependencies, which we don't need to install in order to run the ast transformation

this.configuration.config.webpackOptions[propType] = null;
}
})
.then(() => {
Copy link
Member

Choose a reason for hiding this comment

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

void here _: void

}

public prompting() {
const done: () => void | boolean = this.async();
Copy link
Member

Choose a reason for hiding this comment

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

(_)

Copy link
Member Author

Choose a reason for hiding this comment

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

If you do that then we'll have to pass something when we call done() at the end of generators.

Copy link
Member

Choose a reason for hiding this comment

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

oh okay, let's keep it then (:

@@ -0,0 +1,4 @@
declare module "*.json" {
const value: any;
Copy link
Member

Choose a reason for hiding this comment

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

this is not any, bool | num | object | array

Copy link
Member

Choose a reason for hiding this comment

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

^

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, we have to use any here else it won't be able to read the json file. This is just json-loader because TS doesn't have inbuilt support for importing .json files.
We've defined types while assigning / using json values.

https://github.com/dhruvdutt/webpack-cli/blob/3a711035c0178ab2f7b97e7b3cd3b7d47972a98b/packages/generators/add-generator.ts#L167

Copy link
Member

Choose a reason for hiding this comment

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

* @returns {Object} An Object that holds the answers given by the user, later used to scaffold
*/

export default function entry(self: any, answer: {
Copy link
Member

Choose a reason for hiding this comment

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

generator instance on self

Copy link
Member Author

Choose a reason for hiding this comment

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

As types don't get carried forward in functions, we had to define an explicit interface for Yeoman which could be used here. So, added IYeoman.

const webpackEntryPoint: object = {};
entryIdentifiers = multipleEntriesAnswer.multipleEntries.split(",");

function forEachPromise(entries, fn) {
Copy link
Member

Choose a reason for hiding this comment

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

missing types

loader: string;
options: {
plugins: string[];
presets: any[];
Copy link
Member

Choose a reason for hiding this comment

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

string

@@ -11,7 +11,7 @@ interface IInquirerList extends IScaffoldBaseObject {
}

interface IInquirerInput extends IScaffoldBaseObject {
validate?: Function;
validate?: (input: any) => string | boolean;
Copy link
Member

Choose a reason for hiding this comment

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

input is string

@@ -95,7 +95,7 @@ export function Input(name: string, message: string): IInquirerInput {
};
}

export function InputValidate(name: string, message: string, cb: Function): IInquirerInput {
export function InputValidate(name: string, message: string, cb?: (input: any) => string | boolean): IInquirerInput {
Copy link
Member

Choose a reason for hiding this comment

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

input is string

message: string | ((answers: Object) => string);
choices?: any[] | ((answers: Object) => any);
default?: string | number | string[] | number[] | ((answers: Object) => (string | number | string[] | number[]));
validate?: ((input: any) => boolean | string);
Copy link
Member

Choose a reason for hiding this comment

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

string as input

@webpack-bot
Copy link

Thank you for your pull request! The most important CI builds succeeded, we’ll review the pull request soon.

copyTemplateFiles,
templateFn
export default function addonGenerator(
prompts: any[],
Copy link
Member

Choose a reason for hiding this comment

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

My bet is that these are array of functions or promises, debug it to check

);
}
const packager = getPackageManager();
const packager: string = getPackageManager();
const opts = packager === "yarn" ? { dev: true } : { "save-dev": true };
Copy link
Member

Choose a reason for hiding this comment

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

missing type

configPath = null;
// end the generator stating webpack config not found or to specify the config
}
this.webpackOptions = fs.readFileSync(configPath, "utf8");
Copy link
Member

Choose a reason for hiding this comment

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

I think you'll need to do a JSON parse too, this will be a utf8 encoded string

Copy link
Member Author

@dhruvdutt dhruvdutt Jul 6, 2018

Choose a reason for hiding this comment

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

Yeah. This will return a string. Doing JSON.parse() on that string would result in error because the string contents aren't in JSON convertable format as it would be module.exports = { ... }. Even if we somehow read it in some JS variable, we would have to read object at module.exports.

I think the best way is to do require(configPath). Same as we were doing earlier. It would directly read the webpack config object present at module.export = { ... }.

We should always use require because the file we are reading is a node module.

Copy link
Member

Choose a reason for hiding this comment

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

Yes you are right on the part on requiring the correct module, but when we do a require it will try to resolve dependencies we might not have in the repo and access variables that we might not need to bother with. It will result in a lot of errors for us and that's not good.

Try to implement this using a JSON read instead. It is fully possible. To access the variables you'll need to do something like module['entry'] etc

Copy link
Member Author

Choose a reason for hiding this comment

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

it will try to resolve dependencies we might not have in the repo.

require doesn't always / only resolve npm dependencies. It's also a standard convention for importing modules. If it's a file path like in this case, it won't try to resolve dependencies but simply import the module.exports object.

It will result in a lot of errors for us and that's not good.

We were already using this convention in the existing codebase (master).

configModule = require(configPath);

? require(resolvePluginsPath)

Copy link
Member Author

Choose a reason for hiding this comment

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

Can we have some third opinion @ematipico ?

Copy link
Member

Choose a reason for hiding this comment

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

In that case, it should be changed. @dhruvdutt either way you do this, make sure not to import a file directly, as it would resolve the dependencies. I assume the closest thing you'll get to a error-free case is a stringified and parsed json

type?: string;
}

export interface IWebpackOptions {
Copy link
Member

Choose a reason for hiding this comment

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

These types need to be written as they will be, all of them are strings in the ast types, but here they are specified. Check webpack validation schema for definition of types

Copy link
Member Author

Choose a reason for hiding this comment

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

You mean everything from this file?

Copy link
Member

Choose a reason for hiding this comment

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

Yes that's right

Copy link
Member

Choose a reason for hiding this comment

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

Base your values on that file, avoid using any

@@ -0,0 +1,4 @@
declare module "*.json" {
const value: any;
Copy link
Member

Choose a reason for hiding this comment

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

public run(target: string, options?: object, done?: Function): IRunEnv;
public runInstall(packager: string, dependencies: string[], options?: object): void;
public on(event: string, listener: Function): this;
public async(): () => void | boolean;
Copy link
Member

Choose a reason for hiding this comment

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

is _? void possible here?

public run(target: string, options?: object, done?: Function): IRunEnv;
public runInstall(packager: string, dependencies: string[], options?: object): void;
public on(event: string, listener: Function): this;
public async(): () => void | boolean;
Copy link
Member

Choose a reason for hiding this comment

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

is _?:void possible here?

tslint.json Outdated
@@ -15,6 +15,10 @@
],
"no-console": {
"severity": "warning"
},
"max-line-length": {
"options": { "limit": 120 },
Copy link
Member

Choose a reason for hiding this comment

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

let's keep this to 70 and warn when it's over limit

@evenstensberg
Copy link
Member

Needs a rebase :)

Copy link
Member

@evenstensberg evenstensberg left a comment

Choose a reason for hiding this comment

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

PTAL

.gitignore Outdated
@@ -33,6 +33,3 @@ lerna-debug.log

# Yarn lock file
yarn.lock

# Custom typings
Copy link
Member

Choose a reason for hiding this comment

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

Missing types folder

Copy link
Member Author

Choose a reason for hiding this comment

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

We decided to remove types from root level and move it inside packages. Removing it from gitignore was left.

])
.then(({ propType }) => {
.then(({ propType }: { propType: string }) => {
Copy link
Member

Choose a reason for hiding this comment

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

returns promise

]).then(({ keyType }) => {
Array.from(propValue),
),
]).then(({ keyType }: { keyType: string }) => {
Copy link
Member

Choose a reason for hiding this comment

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

returns promise

Copy link
Member Author

Choose a reason for hiding this comment

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

We aren't returning anything inside this block. It should be void.

])
.then(({ keyType }) => {
.then(({ keyType }: { keyType: string }) => {
Copy link
Member

Choose a reason for hiding this comment

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

returns promise

this.configuration.config.webpackOptions.module = {
rules: [ loader ]
};
.then(({ rule }: { rule: string }) => {
Copy link
Member

Choose a reason for hiding this comment

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

returns promise

Copy link
Member Author

Choose a reason for hiding this comment

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

We aren't returning anything inside this block. It should be void.

),
]),
).then((entryPropAnswer: object) => {
Object.keys(entryPropAnswer).forEach((val: string) => {
Copy link
Member

Choose a reason for hiding this comment

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

returns void

validate,
),
]),
).then((entryPropAnswer: object) => {
Copy link
Member

Choose a reason for hiding this comment

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

returns promise

Copy link
Member Author

Choose a reason for hiding this comment

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

It's actually returning the entryPropAnswer itself / webpackEntryPoint which is an object.

.then(singularEntryAnswer => {
.then((singularEntryAnswer: {
singularEntry: string,
}) => {
Copy link
Member

Choose a reason for hiding this comment

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

returns promise

Copy link
Member Author

Choose a reason for hiding this comment

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

It's actually returning the singularEntry itself which is a string.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

Take a look at this code (from master) where we are returning non-Promise / string.

.then(singularEntryAnswer => {
let { singularEntry } = singularEntryAnswer;
if (singularEntry.indexOf("\"") >= 0) {
singularEntry = singularEntry.replace(/"/g, "'");
}
if (singularEntry.length <= 0) {
self.usingDefaults = true;
}
return singularEntry;
});

There are many such examples where we aren't returning Promise. What should we do in such cases?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think there's some confusion here. The .then returns a promise but the actual callback called inside the .then returns whatever we want.

So if we have

Const p = promise.then(_ => 5)

p is of the type Promise<Number> and the callback returns a type of Number.

To clarify, the callback doesn't return a promise

Copy link
Member

Choose a reason for hiding this comment

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

Okay. Emanuelle cleared it out. Add types for those though ;)

* @returns {Function} An function that returns an array
* that consists of the uglify plugin
*/

module.exports = _ => {
export default function(): string[] {
Copy link
Member

Choose a reason for hiding this comment

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

(_?:void)

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not a promise, so _ should be removed. Can we have a eslint rule?

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

That's Ruby, that treats them differently, what's your point?
Also, underscore is used as temporary placeholder in libraries such as Ramda, where it has a different meaning. My point is, what's the heff meaning of this underscore in this project? Where's documented? Why there's no eslint rule to tell me that the developer has to put in certain cases?

Copy link
Member

Choose a reason for hiding this comment

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

Responded this internally. Needs an issue to be in the desc of the update docs issue

name: string;
message: string;
choices?: ((answers: Object) => any) | any[];
Copy link
Member

Choose a reason for hiding this comment

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

returns array of strings

arr.forEach(p => {
const traverseAndGetProperties = (arr: object[], prop: string): boolean => {
let hasProp: boolean = false;
arr.forEach((p: object) => {
Copy link
Member

Choose a reason for hiding this comment

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

returns void

PROPS.filter(food =>
food.toLowerCase().includes(input.toLowerCase())
)
PROPS.filter((prop: string) =>
Copy link
Member

Choose a reason for hiding this comment

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

missing ret type for filter

// Array -> Object -> Merge objects into one for compat in manualOrListInput
defOrPropDescription = Object.keys(defOrPropDescription[0].enum)
.map(p => {
.map((p: string) => {
Copy link
Member

Choose a reason for hiding this comment

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

missing ret on map

);
})
.reduce((result, currentObject) => {
for (let key in currentObject) {
.reduce((result: object, currentObject: object) => {
Copy link
Member

Choose a reason for hiding this comment

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

missing return for reduce

])
.map(p =>
.map((p: string) =>
Copy link
Member

Choose a reason for hiding this comment

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

map missing return

],
["src/_index.js.tpl", "examples/simple/_webpack.config.js.tpl"],
gen => ({ name: _.upperFirst(_.camelCase(gen.props.name)) })
(gen: IYeoman) => ({ name: _.upperFirst(_.camelCase(gen.props.name)) }),
Copy link
Member

Choose a reason for hiding this comment

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

missing return type

if (this.webpackOptions.module && this.webpackOptions.module.rules) {
return this.webpackOptions.module.rules.map((rule: {
loader: string;
}) => rule ? rule.loader : null);
Copy link
Member

Choose a reason for hiding this comment

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

missing ret type

Copy link
Member Author

Choose a reason for hiding this comment

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

Type is already present as an object.

if (n) {
Object.keys(n).forEach(val => {
Object.keys(n).forEach((val: string) => {
Copy link
Member

Choose a reason for hiding this comment

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

missing return type

* @returns {Function} A callable function that adds the babel-loader with env preset
*/
export default function(): IModule {
return {
Copy link
Member

Choose a reason for hiding this comment

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

returns obj

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah! interface IModule extends Object :)

@@ -8,10 +6,10 @@
* @returns {String | Boolean } Returns truthy if its long enough
* Or a string if the user hasn't written anything
*/
module.exports = value => {
export default function validate(value: string): string | boolean {
const pass = value.length;
Copy link
Member

Choose a reason for hiding this comment

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

missing type on const

@dhruvdutt dhruvdutt force-pushed the typescript-generators branch from 7dad2d4 to 75f018e Compare July 13, 2018 07:42
Copy link
Member

@evenstensberg evenstensberg left a comment

Choose a reason for hiding this comment

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

lgtm

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

Successfully merging this pull request may close these issues.

4 participants