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

Workspaces phase 2: executing commands #3365

Merged
merged 21 commits into from
May 24, 2017
Merged

Conversation

arcanis
Copy link
Member

@arcanis arcanis commented May 10, 2017

The current implementation allows to run a command inside the context of a workspace, regardless where the user is inside the project:

$> yarn workspace babel-runtime add babel-core

@arcanis arcanis requested a review from bestander May 10, 2017 14:17
@arcanis
Copy link
Member Author

arcanis commented May 10, 2017

There might be flow / lint issues, I'll fix them before merging.

@bestander
Copy link
Member

Awesome!
Will look at it later today

@bestander
Copy link
Member

How about some tests?
See new test file in #3229

@arcanis
Copy link
Member Author

arcanis commented May 10, 2017

Will git see that these are the same files even if on different branches?

@bestander
Copy link
Member

no, you'll probably need to rebase and resolve conflicts after my branch is merged

@bestander
Copy link
Member

but you can probably rebase on top of my branch

@arcanis
Copy link
Member Author

arcanis commented May 10, 2017

If I do this it'll cause issues if you rebase yours later :/ I'll try to add a commit with your fixtures, and remove it before merging if git doesn't like it.

}

try {
await child.spawn(process.argv[0], [process.argv[1], ... rest], { stdio: 'inherit' });
Copy link
Member

Choose a reason for hiding this comment

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

Smart move :)
But I don't think we want to necessarily shell out to start another node VM.
I would prefer us doing it via requires.

Copy link
Member Author

Choose a reason for hiding this comment

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

There's quite a bit of logic that we currently can't run in the same process (most notably, everything related to the command line parsing, which is run in src/cli/index.js). That could be refactored, but I felt it could be too heavy for prototyping the command.

Copy link
Member

Choose a reason for hiding this comment

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

Got it, I think workspace command won't interfere with regular workflows, looks fine then

@arcanis
Copy link
Member Author

arcanis commented May 15, 2017

@bestander I've just rebased on master (ie on your merged PR), and added a commit that implements a project detection logic inside install (which is then used by add & remove). Could you review the implementation, except on the following points?

  • I need to avoid computing the project path multiple times. It's really bad for performances. Ideally, it should only be computed once. Two solutions: either it is computed in the Config.init() function, or I can add a cache to findProject. I like the first solution better.

  • I need to write some tests to make sure everything's acting correctly, but the manual tests I've made seem to indicate that everything looks right.

  • I need to blacklist node_modules inside resolveWorkspaces, otherwise we might end up picking up the package.json files of our dependencies ... not cool 😄

@@ -204,7 +204,7 @@ export async function run(
throw new MessageError(reporter.lang('missingAddDependencies'));
}

const lockfile = await Lockfile.fromDirectory(config.cwd, reporter);
const lockfile = await Lockfile.fromDirectory(config, config.cwd, reporter);
Copy link
Member

Choose a reason for hiding this comment

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

config.cwd looks redundant here, do we ever need it not from config?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not really, but it makes sense semantically to specify the path, otherwise it should be renamed to fromConfig - and as you mentioned in the comment below, it would probably be for the best to keep this function not too tightly coupled with the config object.

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 would it be better to pass no the full config to the fromDirectory function?
Technically you need only the config.worktreeFolder || config.cwd and is dir needed then?

Copy link
Member

@bestander bestander left a comment

Choose a reason for hiding this comment

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

Looks quite simple, so let's do some e2e tests

@@ -86,9 +87,12 @@ export default class Lockfile {
[key: string]: LockManifest
};

static async fromDirectory(dir: string, reporter?: Reporter): Promise<Lockfile> {
static async fromDirectory(config: Config, dir: string, reporter?: Reporter): Promise<Lockfile> {
Copy link
Member

Choose a reason for hiding this comment

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

nit: this couples lockfile wrapper to config, not sure if it is a concern but maybe we'll want to decouple it back in the future

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, it should be fairly easy. I think most tools currently stored inside the config object (such as tryManifest, etc) could be safely moved somewhere else (for example in miniyarn, I had a util/yarn file to store all such functions), so that the config would only contain the configuration data and nothing else.

}

try {
await child.spawn(process.argv[0], [process.argv[1], ... rest], { stdio: 'inherit' });
Copy link
Member

Choose a reason for hiding this comment

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

Got it, I think workspace command won't interfere with regular workflows, looks fine then


try {
await child.spawn(process.argv[0], [process.argv[1], ... rest], { stdio: 'inherit' });
} catch (err) {
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 don't want to gobble up the error here?

Copy link
Member Author

@arcanis arcanis May 17, 2017

Choose a reason for hiding this comment

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

The issue I had was that regular errors are already handled by the spawned child. Discarding the error here was a nice way to prevent the error from being printed twice:

screen shot 2017-05-17 at 11 08 09 am

As you can see, if I don't catch the error, I get the child info, which make little sense in the context of the workspace command (it should be an hidden implementation detail).

However, catching the error unfortunately also swallows the exit code ... what would you think about manually calling process.exit in the catch block, with the exit code returned by the child?

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 double error is ok-ish as long as it does not show the same error message twice.
I would vote against calling process.exit in multiple places in the code, better leave only one exit code in index.js, this makes code hard to reuse

@arcanis
Copy link
Member Author

arcanis commented May 18, 2017

I fixed the issue I raised above, rebased, and implemented the exec command. However, I'm not sure I'll be able to write the tests right now:

  • The current implementation forwards stdio to the spawn process (which is useful, because it allows the spawn processes to interact with the user, which would break otherwise). Unfortunately, it means that it bypasses the reporters, so I can't inspect to make sure that the output is the one I expect. I tried to workaround this by inspecting the state of the filesystem, but:

  • The current implementation uses process.argv[0] and process.argv[1] in order to spawn a new instance of Yarn. Unfortunately, doing this cannot work when running Jest, since it will run Jest instead of Yarn.

src/config.js Outdated

// We need the ignoreFiles, not the keepFiles, because the patterns are exclusion patterns rather than the opposite
const ignoreBasenames = new Set(this.registryFolders);
const files = filter.sortFilter(await fs.walk(root, undefined, ignoreBasenames), compiledPatterns).ignoreFiles;
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 merge this code with the one used in install command https://github.com/yarnpkg/yarn/blob/master/src/cli/commands/install.js#L238.

I think using glob is better than ad-hoc fs.walk because the JS community is quite familiar with glob notation.

Copy link
Member Author

Choose a reason for hiding this comment

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

The code above is also using the glob syntax, because of sortFilters. I tried to use the same implementation from what we use for pack, but maybe glob is more efficient, yeah.

Copy link
Member Author

Choose a reason for hiding this comment

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

Should be fine now 👍 Ok to merge?

}

async findWorktree(initial: string): Promise<?string> {
let previous = null;
Copy link
Member

Choose a reason for hiding this comment

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

install command is guarded by projectManifestJson.workspaces && this.config.workspacesExperimental.
How about extracting it in a setting and checking it here?

Copy link
Member

Choose a reason for hiding this comment

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

Ping about the question above.

I think it is related to my previous comment regarding config.worktreeFolder || config.cwd.
Should resolving workspaces and root folders be hidden behind the configuration flag and whether workspaces is enabled in the root manifest?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure what you mean with projectManifestJson.workspaces - if we're not in a workspace (ie. if there is no manifest with a workspaces key), then findWorktree will return null. Do you want it to do something different?

I'll add a check on the experimental flag, tho.

Copy link
Member

Choose a reason for hiding this comment

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

I guess I got my thoughts mixed up a bit :)
I think resolveWorkspaces should check for experimental flag and private field.
Then this check would propagate to be implicit everywhere workspaces are used.

Copy link
Member Author

@arcanis arcanis May 22, 2017

Choose a reason for hiding this comment

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

Hm I was thinking of implementing this check inside Config#init, would you be ok with that? The problem with resolveWorkspaces is that it doesn't have to be executed in every case (for example, when adding a dependency we don't use it, since we only care about the current workspace and the base worktree).

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, definitely, you are correct

@@ -660,7 +660,7 @@ export class Install {
}

// build lockfile location
const loc = path.join(this.config.cwd, constants.LOCKFILE_FILENAME);
const loc = path.join(this.config.worktreeFolder || this.config.cwd, constants.LOCKFILE_FILENAME);
Copy link
Member

Choose a reason for hiding this comment

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

The this.config.worktreeFolder || this.config.cwd expression is used in multiple places.
How about moving it to a function that would be self explaining?
Should we make this.config.cwd private?

Copy link
Member Author

@arcanis arcanis May 22, 2017

Choose a reason for hiding this comment

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

Hm, I think right now it's fine, but maybe that's something that could be improved later.

I was actually thinking that maybe we could add another variable, workspaceFolder, that would contain the path to the top-most workspace, and use it instead of cwd about everywhere, except of course yarn init.

The difference with cwd would be that if you're in a subdirectory, then the workspaceFolder would always target the directory that contains the package.json file. This way, people wouldn't have the issue of adding dependencies to their subfolders instead of their project, if they're in the wrong directory.

But that would be a BC, so we should probably discuss this in an other PR/RFC

Copy link
Member

Choose a reason for hiding this comment

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

The question is - someone adds a new command, say amazing-command, do they need to pass this.config.worktreeFolder || this.config.cwd when reading lockfile?

It looks like we leak workspaces implementation detail into every command, seems like a red flag and besides, we have config.worktreeFolder || repeated 13 times already. It definitely asks for encapsulation

Copy link
Member Author

@arcanis arcanis May 22, 2017

Choose a reason for hiding this comment

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

Here are the options (I assume in these options that there is a workspaceFolder variable, but it work the same way if we don't):

  1. Keep things as they are (eh), with the worktree definition being "the closest location where lies the workspaces field", and explicitely specify the locations from which we want to read the lockfile

  2. Redefine worktreeFolder as being "the closest location where lies the lockfile", which would effectively worktreeFolder || workspaceFolder || cwd - the reason I didn't do this is that it means that a project with no workspaces would internally work just like a project with a single workspace (we wouldn't be able to distinguish a project that doesn't use workspaces like we currently can with worktreeFolder === null). That's not a problem strictly speaking, but we haven't specified this in our RFCs so we have to agree on this before I can do this.

  3. Add a new variable (let's say lockfileFolder) that would be defined as worktreeFolder || workspaceFolder || cwd. The only issue I see with this is that it would mean yet another folder variable.

What do you think? My order of preference is 3 > 1 > 2

Copy link
Member

Choose a reason for hiding this comment

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

lockfileFolder sounds quite descriptive, I like it, let's try 3

Copy link
Member Author

Choose a reason for hiding this comment

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

👍

@bestander
Copy link
Member

Great progress, @arcanis.
We need more tests ;)

@bestander bestander changed the title Adds yarn workspace command Workspaces phase 2: executing commands May 19, 2017
@bestander
Copy link
Member

bestander commented May 20, 2017

-- updated comment after thinking a bit --

I am working on phase 3, there is a need to preserve the workspaces structure during whole installation from fetch phase till linking.

In fetchRequestFromCwd at https://github.com/yarnpkg/yarn/blob/master/src/cli/commands/install.js#L233 InstallCwdRequest is returned.
I wonder if this structure could be preserved through the whole installation and passed to linking.


const workspaces = await config.resolveWorkspaces(worktreeFolder, manifest.workspaces);

if (args.length < 1) {
Copy link

Choose a reason for hiding this comment

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

we could move args checks before await config.resolveWorkspaces(worktreeFolder, manifest.workspaces), will eliminate some extra work this way.

// read the manifest in this directory
const lockfileLoc = path.join(dir, constants.LOCKFILE_FILENAME);
const lockfileLoc = path.join(config.worktreeFolder || config.cwd, constants.LOCKFILE_FILENAME);
Copy link

Choose a reason for hiding this comment

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

dir is not used anymore, what is the reason to keep it as a parameter?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch! This code became boggus after I refactored it a few times. I've now restored the old code, since we don't use config in this function anymore 👍


expect(await fs.exists(`${config.cwd}/yarn.lock`)).toEqual(true);
expect(await fs.exists(`${config.cwd}/packages/package-b/yarn.lock`)).toEqual(false);
});
Copy link
Member

Choose a reason for hiding this comment

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

You also need to check in which package.json left-pad/right-pad entries were added.
And run yarn add from the root.

I think https://github.com/yarnpkg/yarn/blob/master/__tests__/commands/install/workspaces-install.js is a better location for this test as you are not testing workspace command here

Copy link
Member

@bestander bestander left a comment

Choose a reason for hiding this comment

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

Just a few small concerns about paths and config

}

async findWorktree(initial: string): Promise<?string> {
let previous = null;
Copy link
Member

Choose a reason for hiding this comment

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

Ping about the question above.

I think it is related to my previous comment regarding config.worktreeFolder || config.cwd.
Should resolving workspaces and root folders be hidden behind the configuration flag and whether workspaces is enabled in the root manifest?

@arcanis
Copy link
Member Author

arcanis commented May 24, 2017

@bestander Everything should be fixed 👍

Copy link
Member

@bestander bestander left a comment

Choose a reason for hiding this comment

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

Awesome!
Merge once tests are green

@arcanis arcanis merged commit 8ede3c4 into yarnpkg:master May 24, 2017
@bestander
Copy link
Member

Hey @arcanis, I think we lost this feature.
Running yarn workspace jest-matcher-utils add isarray does not run the add from workspace anymore.

@jkillian
Copy link

Ran into this same bug today, file a short ticket over at #4021

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.

4 participants