-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Conversation
There might be flow / lint issues, I'll fix them before merging. |
Awesome! |
How about some tests? |
Will git see that these are the same files even if on different branches? |
no, you'll probably need to rebase and resolve conflicts after my branch is merged |
but you can probably rebase on top of my branch |
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. |
src/cli/commands/workspace.js
Outdated
} | ||
|
||
try { | ||
await child.spawn(process.argv[0], [process.argv[1], ... rest], { stdio: 'inherit' }); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@bestander I've just rebased on master (ie on your merged PR), and added a commit that implements a project detection logic inside
|
src/cli/commands/add.js
Outdated
@@ -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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this 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
src/lockfile/wrapper.js
Outdated
@@ -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> { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/cli/commands/workspace.js
Outdated
} | ||
|
||
try { | ||
await child.spawn(process.argv[0], [process.argv[1], ... rest], { stdio: 'inherit' }); |
There was a problem hiding this comment.
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
src/cli/commands/workspace.js
Outdated
|
||
try { | ||
await child.spawn(process.argv[0], [process.argv[1], ... rest], { stdio: 'inherit' }); | ||
} catch (err) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
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?
There was a problem hiding this comment.
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
I fixed the issue I raised above, rebased, and implemented the
|
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
src/cli/commands/install.js
Outdated
@@ -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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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):
-
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 -
Redefine
worktreeFolder
as being "the closest location where lies the lockfile", which would effectivelyworktreeFolder || 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 withworktreeFolder === 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. -
Add a new variable (let's say
lockfileFolder
) that would be defined asworktreeFolder || 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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
Great progress, @arcanis. |
-- 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. |
src/cli/commands/workspace.js
Outdated
|
||
const workspaces = await config.resolveWorkspaces(worktreeFolder, manifest.workspaces); | ||
|
||
if (args.length < 1) { |
There was a problem hiding this comment.
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.
src/lockfile/wrapper.js
Outdated
// 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); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 👍
__tests__/commands/workspace.js
Outdated
|
||
expect(await fs.exists(`${config.cwd}/yarn.lock`)).toEqual(true); | ||
expect(await fs.exists(`${config.cwd}/packages/package-b/yarn.lock`)).toEqual(false); | ||
}); |
There was a problem hiding this comment.
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
There was a problem hiding this 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; |
There was a problem hiding this comment.
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?
@bestander Everything should be fixed 👍 |
There was a problem hiding this 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
Hey @arcanis, I think we lost this feature. |
Ran into this same bug today, file a short ticket over at #4021 |
The current implementation allows to run a command inside the context of a workspace, regardless where the user is inside the project: