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(cli): auto-generate index.ts for exports #1363

Merged
merged 1 commit into from
Jun 4, 2018
Merged

feat(cli): auto-generate index.ts for exports #1363

merged 1 commit into from
Jun 4, 2018

Conversation

virkt25
Copy link
Contributor

@virkt25 virkt25 commented May 25, 2018

This is part of the work I did for the Boot Spike. lb4 datasource makes use of the index generation so decided to just deliver this as it's own feature.

fix #1127

UPDATE: MAY 31

  • The PR no longer re-generated index.ts -- If already present, it adds a line at the end. If it doesn't exist in the folder it creates it and adds the file to to it.
  • Auto-generation can be skipped as documented in code

Checklist

  • npm test passes on your machine
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

@virkt25 virkt25 changed the title Indexer feat(cli): auto-generate index.ts for exports May 25, 2018
@virkt25 virkt25 requested a review from a team May 25, 2018 04:28
@virkt25 virkt25 self-assigned this May 25, 2018
@virkt25 virkt25 added this to the May Milestone milestone May 25, 2018
Copy link
Contributor

@shimks shimks left a comment

Choose a reason for hiding this comment

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

👏👏👏 Small QoL comments

@@ -235,6 +235,7 @@ module.exports = class ControllerGenerator extends ArtifactGenerator {
'Controller %s is now created in src/controllers/',
this.artifactInfo.name,
);
super.updateIndexFile();
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be returned since it's a promise? It might not matter since it's the last thing being done here, but I think it makes sense to return it for the sake of using a promise

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, shouldn't updateIndexFile be called in ArtifactGenerator since most artifacts seem to use index.ts?

const outFile = `${dir}/index.ts`;
debug(`outFile => ${outFile}`);

const writeFileAsync = util.promisify(fs.writeFile);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any reason as to why we don't use fs.writeFileSync?


async updateIndexFile() {
await index(this.artifactInfo.outDir, {
prefix: `.${this.artifactInfo.type}`,
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 weird to me to have to . as part of prefix. I think it's better for . to be inferred when setting globPattern in make-index.js

@virkt25 virkt25 force-pushed the indexer branch 2 times, most recently from 19ee9fb to 6932169 Compare May 26, 2018 04:39
Copy link
Member

@bajtos bajtos left a comment

Choose a reason for hiding this comment

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

I have few high-level concerns/comments:

(1) Is there a reasonably-easy way how to test make-index?

(2) Is it a good idea to force index.ts file to be auto-generated? What if app developers decide to add more files, perhaps in a deeper directory structure, and want to export only subset of entities from those additional files?

Instead of generating the whole file, can we simply append new export * from lines at the bottom?

// Copyright IBM Corp. 2017,2018. All Rights Reserved.
// Node module: @loopback/example-todo
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't the example files preserve license headers? (I understand the files generated by lb4 do not come with license headers.)

@@ -1,6 +0,0 @@
{
"bracketSpacing": false,
Copy link
Member

Choose a reason for hiding this comment

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

What is the reason for removing .prettierrc from this project? I am not against such change per se, but would like to better understand the reasoning. It may be better to make this change in a standalone pull request.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason was that this .prettierrc uses different formatting that the top-level one we use for the project and I noticed that and removed it. I've reverted this change and will make a new PR for this.

// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

const prettier = require('prettier');
Copy link
Member

Choose a reason for hiding this comment

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

Cross-posting item 8 from #441 (comment):

Formatting of generated code: it would be great if our tooling was able to pick up prettier version and configuration from the project we are running in and format the generated code accordingly.

Imagine a user is using prettier in their project (as scaffolded by lb4 app), but changed prettier config to use double-quotes and tabs. If we don't honor their style, they will have to run npm run lint:fix every time they add a new model or update auto-generated TypeScript definitions.

It is also important to run prettier from node_modules of the target project, because different prettier version have variations in how they format the same piece of code and we cannot get all users to have exactly the same version of prettier as we use in LB4 tooling.

It may be enough to add ${this.destinationPath()}/node_modules to the list of directories where Node.js looks for modules. See how Mocha handles that (link):

module.paths.push(cwd, join(cwd, 'node_modules'));

I am ok to leave these two improvements out of scope of this initial pull request as long as there is a follow-up issue (or two?) to ensure we fix this problem as part of the DP3 Boot epic.

nested: true,
prefix: '',
commentHeader: `This is an auto-generated file. DO NOT EDIT.`,
commentFooter: `This is an auto-generated file. DO NOT EDIT.`,
Copy link
Member

Choose a reason for hiding this comment

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

Is there any particular reason for using backticks (template literals) instead of regular single-quotes?

files.forEach(file => {
const relPath = `export * from './${path
.relative(dir, file)
.slice(0, -3)}';`;
Copy link
Member

Choose a reason for hiding this comment

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

Please extract the expression computing the actual path outside of the template literal and save the result to a variable.

const relativePath = path.relative(dir, file).slice(0, -3);
const exportLine = `export * from '${relativePath}';`;
lines.push(relPath);

debug(`outFile => ${outFile}`);

const writeFileAsync = util.promisify(fs.writeFile);
await writeFileAsync(outFile, code);
Copy link
Member

Choose a reason for hiding this comment

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

This will break incremental builds, because every run of make-index is going to change the modification time of index.ts to the current date, even in cases where the file content was not changed at all.

Proposed solution:

  1. Read the current file content and if the old content is the same as the new content, then don't write anything. I believe this is what prettier is doing too.
  2. Set mtime of the written file to match the latest mtime of files we were generating index from. I.e. for each file in dir (excluding index.ts), obtain mtime value and then pick the maximum value of all mtimes.

I think the second item is not enough on its own, because file watchers like tsc --watch, fs-events, etc. listen for write events and ignore mtime.

The first item may is most likely good enough, because typically the index.ts file would be updated shortly after the last non-index file was added/updated.

Copy link
Member

Choose a reason for hiding this comment

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

I am ok to leave these two improvements out of scope of this initial pull request as long as there is a follow-up issue to ensure we fix this problem as part of the DP3 Boot epic.

const comments = comment.split('\n');
let normalizedComment = '';

// For each comment line -- if it's a comment we
Copy link
Member

Choose a reason for hiding this comment

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

if it's a comment we

We do what? Looks like you accidentally forgot to finish the sentence here.

Copy link
Contributor

@shimks shimks left a comment

Choose a reason for hiding this comment

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

Looks good at high-level. Couple of thoughts

@@ -0,0 +1 @@
export * from './ping.controller';
Copy link
Contributor

Choose a reason for hiding this comment

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

This makes me wonder whether it should be included when the app is generated as opposed to adding it on when the controller generator is run. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Check the file -- It's in the app generator and this is just the default index.ts that gets generated since we are generating ping.controller in new apps.

Copy link
Contributor

Choose a reason for hiding this comment

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

The ping controller I know about, but I was wondering about whether we should include this index.ts file when the app generator is run

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea when the app generator is run there will be a src/controllers/index.ts that will be generated which exports the ping controller.

const path = require('path');
const util = require('util');
const fs = require('fs');
const appendFileAsync = util.promisify(fs.appendFile);
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

appendFileSync is a blocking / synchronous operation. async / await is a promise under the hood even though it appears to be synchronous when written.

From https://hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9

Async/await makes asynchronous code look and behave a little more like synchronous code. This is where all its power lies.

In a generator you can call updateIndex without an await ... and have more code below -- and that additional code and continue running without being blocked essentially.

Here's a Node.js article on blocking / non-blocking. https://nodejs.org/en/docs/guides/blocking-vs-non-blocking/

throw new Error(`${file} must be a TypeScript (.ts) file`);
}
const content = `export * from './${file.slice(0, -3)}';\n`;
await appendFileAsync(indexFile, content);
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we explicitly check whether the line exists already? Or is it that too much of an edge case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe. My assumption is to let the generator handle that -- within a generator this function is triggered only when the generationStatus is not an error / identical file. So if the file exists this won't be called. -- That said another check doesn't hurt. 🤷‍♂️

Copy link
Contributor

Choose a reason for hiding this comment

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

Nvm, I forgot about yeoman's own conflicter system

const expectedFile = path.join(SANDBOX_PATH, 'index.ts');

describe('update-index unit tests', () => {
beforeEach('reset sandbox', () => sandbox.reset());
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to just provide sandbox.reset since it isn't async anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've tried and I get the following error message if I do beforeEach(sandbox.reset):

1) update-index unit tests
       "before each" hook: reset for "creates index.ts when not present":
     TypeError: this.validateInst is not a function
      at Context.reset (packages/testlab/src/test-sandbox.ts:31:27)

Copy link
Contributor

@shimks shimks Jun 1, 2018

Choose a reason for hiding this comment

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

Looks like it has to do with scoping :(.
On the other hand, do you think async () => await sandbox.reset() is cleaner than () => sandbox.reset())? I feel we might as well use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like the current format -- it's how we use sandbox.reset in the beforeEach hooks of other tests. Consistency is good IMO

});

it('throws an error when given a non-ts file', async () => {
expect(updateIndex(SANDBOX_PATH, 'test.js')).to.be.rejectedWith(
Copy link
Contributor

Choose a reason for hiding this comment

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

needs to be wrapped in an anonymous function

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why?

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 I was thinking of error assertions. pls ignore

@virkt25 virkt25 modified the milestones: May Milestone, June Milestone Jun 2, 2018
@virkt25 virkt25 mentioned this pull request Jun 3, 2018
13 tasks
@virkt25 virkt25 merged commit 2998363 into master Jun 4, 2018
@bajtos bajtos deleted the indexer branch June 5, 2018 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[CLI] Generating index.ts in each models/repositories/controllers automatically
3 participants