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

fix(vite): do not use merged options for preview's build target #14815

Conversation

dmitry-stepanenko
Copy link
Contributor

Current Behavior

Right now options that are provided for the preview target are being merged with build target ones and used for both build and preview targets.

Expected Behavior

This might not be correct, because in some cases we do not want the same to be applied for both targets and this behavior is not straightforward.

Moreover, there's an existing mechanism for customization of a target using configurations, so instead of passing certain options directly to the preview target and expect them to be applied to the build one, we should use something like buildTarget: "build:preview", where preview configuration of a build target would apply everything itself.

Related Issue(s)

Fixes #

@vercel
Copy link

vercel bot commented Feb 6, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

1 Ignored Deployment
Name Status Preview Comments Updated
nx-dev ⬜️ Ignored (Inspect) Feb 6, 2023 at 11:13AM (UTC)

@dmitry-stepanenko
Copy link
Contributor Author

cc @jaysoo @mandarini

@mandarini mandarini self-assigned this Feb 7, 2023
@mandarini mandarini self-requested a review February 7, 2023 08:20
@mandarini mandarini merged commit 8d87654 into nrwl:master Feb 7, 2023
@mandarini
Copy link
Member

@vicb I merged this. I don't know what you think about this, let me know if you think it makes sense! :D

@vicb
Copy link
Contributor

vicb commented Feb 7, 2023

Thanks @mandarini to ping me here!

TL;DR

I think this PR is wrong and should be reverted.

Yet I think I understand where @dmitry-stepanenko comes from and the code might be improved.

Details

Why this is wrong ?

First because some of the build options can be overridden in the preview options - namely logLevel and mode. Note that this is also true for serve vs build targets.

So if we do not merge the options the overrides will not work.

Second the merged options are computed as

  const mergedOptions = {
    ...{ watch: {} },
    ...buildTargetOptions,
    ...options,
  };

The purpose of ...{ watch: {} }, is to make watch mode the default in preview (it can be overridden by explicitly setting watch to false/null in the build target).

What could be improved?

With the following code:

  const mergedOptions = {
    ...{ watch: {} },
    ...buildTargetOptions,
    ...options,
  };

The ...options are the preview target configuration. It basically means that any key defined in the preview target can override the key in the ...buildTargetOptions, with the same name.

As of today nothing prevents you from adding an options that is not in the schema of the preview target. As an example you could add a "sourceMap": false, in the preview target options and Nx will not complain.

Notes:

  • This is an undocumented behavior, the only you are supposed to set in the preview are those
  • This behavior was present before my changes in the serve target. I just replicated the same behavior for the preview target.

So the most correct way would be:

  • validate the options (for each target) against the schema.json - that way users would not be able to set an option that is not documented,
  • keep using the merged options as we want to be able to override some options and we want watch mode to be enabled by default for the preview server.

Is it worth the extra effort?

I did notice this little inconsistency when working on the code but I didn't think it was worth "fixing". That being said I don't think there is any cons of adding this validation against the schema. It could make things a little cleaner and also could catch typos in the target configuration.

However if we decide we want to validate against the schema.json I think it would make little sense to add the code to the vite executors specifically. My opinion is that this validation should be added to the Nx framework so that all the executors (vs vite executors only) are checked.

I hope this make sense?
If not or you need clarification just ask me :)

@mandarini
Copy link
Member

Thank you @vicb !

@dmitry-stepanenko can you please add your comments here, too?

Regarding the "options validation against schema.json", we don't want to do that. We want to allow the user to pass any options they may need, that are supported, for example in this case, by the Vite CLI, without us having to keep an exhaustive schema.json of ALL the available options, and maintain it as such. Same goes for other executors that pass down CLI options (eg. storybook, etc).

That being said, what do you think would be the best way to improve our implementation in this case? Do you think there's something undocumented that we could document in a better/more understandable way?

@vicb
Copy link
Contributor

vicb commented Feb 7, 2023

We want to allow the user to pass any options they may need, that are supported, for example in this case, by the Vite CLI

That is not supported today:

const buildConfig = mergeConfig(
getViteSharedConfig(normalizedOptions, false, context),
{
build: getViteBuildOptions(normalizedOptions, context),
}
);
const watcherOrOutput = await runInstance(buildConfig);

getViteSharedConfig and getViteBuildOptions both return a fixed set of keys. Users can not add arbitrary ones.

@dmitry-stepanenko
Copy link
Contributor Author

dmitry-stepanenko commented Feb 7, 2023

Thanks @vicb. Unfortunately this PR is not aimed at simple code improvement, I'm addressing an issue that cannot be solved without these changes.

The problem is this statement is not always true

It basically means that any key defined in the preview target can override the key in the ...buildTargetOptions, with the same name.

The actual state of things is

It basically means that any key defined in the preview target WILL override the key in the ...buildTargetOptions, with the same name.

I don't think that loosing an ability to control targets separately is correct. I have a scenario where I have to use the build target with "ssr": "src/entry.preview.tsx" option (it is specified in the build target's configuration) and at the same time preview target should not receive it. If we're using merged options, we can either omit it or have the same value specified for both targets.

@vicb
Copy link
Contributor

vicb commented Feb 7, 2023

The actual state of things is

It basically means that any key defined in the preview target WILL override the key in the ...buildTargetOptions, with the same name.

That's exactly what I meant - we are aligned here.

I have to use the build target with "ssr": "src/entry.preview.tsx" option (it is specified in the build target's configuration) and at the same time preview target should not receive it

If I understand correctly you want "ssr": "src/entry.preview.tsx" when you build but you want to preview without ssr set.

The promise of preview is to serve what you build. So if you do not want to use ssr with your preview then you have to create a build target without ssr. For example a nossr build configuration and point to that configuration in your preview with "buildTarget": "<appName>:build:nossr".

But I might miss something about your setup?

@dmitry-stepanenko
Copy link
Contributor Author

Yes, my setup is not standard, vite executors are being used for the qwik nx wrapper. In native Qwik applications build command is invoked twice: for the client and server side setups. These commands should be invoked in the specified order. We're having the same behavior with 2 build targets: build and build-ssr. buildTarget for the preview is the latter one

"build-ssr": {
  "executor": "@nrwl/vite:build",
  "options": {
    "outputPath": "dist/apps/cart",
    "ssr": "src/entry.preview.tsx"
  },
  "dependsOn": [ "build" ]
}

In my case another option would be to combine "build" and "build-ssr" targets into one custom executor, but that will create other set of questions in terms of @nrwl/vite:build:preview-server, as we're expecting buildTarget to have the @nrwl/vite:build executor

@vicb
Copy link
Contributor

vicb commented Feb 7, 2023

If I understand correctly:

  • build-ssr builds your server side
  • build builds your client side
  • you want to preview the server side

Is that correct? (kind of hard to understand without the full config and without more details).

@mandarini
Copy link
Member

I will look at the outcome of this conversation tomorrow, because it is late for me! Please do let me know how I can adjust our code to make things better, and please let me know if I will need to revert the commit, or push some changes! Thank you both!!!!

@vicb
Copy link
Contributor

vicb commented Feb 7, 2023

For now I would revert this PR because it will affect everybody using the vite preview executor

In parallel @dmitry-stepanenko should create an issue to discuss about his "not standard" setup and the best way to support that.

Edit: Some more context: The idea of the vite/preview executor is to mirror what the CLI command vite preview does. And what it does is 1) build the project 2) launch a file server to serve what has been built. Here it seems that what Dmitriy is looking to is to build 2 different projects and serve something (I'm not sure what exactly from the previous discussion). I don't think this is something that vite preview can do and I see no reason why the vite/preview executor should support this. I think that what Dmitriy is looking for is a custom executor based on nx:run-commands that would run 2 vite:build followed by one @nrwl/web:file-server.

@mandarini
Copy link
Member

Ok, @vicb @dmitry-stepanenko I am reverting. Let's see, in any case, how I can improve the whole options implementation, ok?

@mandarini
Copy link
Member

@dmitry-stepanenko please reach out to me/jack on the Community Slack to see what we can do!

@dmitry-stepanenko
Copy link
Contributor Author

dmitry-stepanenko commented Feb 8, 2023

@vicb @mandarini I understand Victor's point of having the same options for build and preview targets, as discussed above, this makes sense in most "standard" scenarios. However, I do believe that we should also be able to support use cases beyond this. So I would suggest adding a customBuildTarget: boolean option, depending on which we can be a bit more flexible about the params that we're feeding to the preview server internally. This also preserves the behavior that was originally provided by Victor.

Here're the changes that I think should do the job, please let me know if this works for you guys https://github.com/nrwl/nx/compare/master...dmitry-stepanenko:ds/vite-preview-custom-build-target?expand=1

@mandarini
Copy link
Member

I will not have time to review this until tonight or tomorrow. If you can both wait, I can devote tomorrow understanding what's going on, because I haven't had time yet to look into the issue, and I have been relying on your suggestions only (which is fine, but I really should look into it myself too).

So, yeah, I'll put a pause here, and will dive into this tomorrow, devote it the appropriate time, and come back to you both!

Thank youuuuu!!!

@dmitry-stepanenko
Copy link
Contributor Author

@vicb just some more context for you to understand my problem. The goal that I'm following is to go through these commands sequentially using @nrwl/vite executors:

  1. vite build
  2. vite build --ssr src/entry.preview.tsx"
  3. vite preview

You can see this in action here https://stackblitz.com/edit/qwik-starter

You can also play around with a real app by running npx create-nx-workspace@latest --qwikAppName=myapp --preset=qwik-nx. You will see that there will be a preview target that currently uses nx:run-commands. Switching to the preview-server executor will break it, because there're some certain parts of processing that is being done based on ssr option by qwikVite plugin.

Using changes that I referenced above it is possible to configure it like this

    "preview": {
      "executor": "@nrwl/vite:preview-server",
      "options": {
        "buildTarget": "myapp:build-ssr",
        "customBuildTarget": true
      },
      "dependsOn": ["build"]
    },

This also opens a perspective for me to wrap 2 build commands into 1 custom executor at some point, if we decide to.

@vicb
Copy link
Contributor

vicb commented Feb 8, 2023

@dmitry-stepanenko thanks for the details I think I have a better understanding of what you need now.

The vite config in https://stackblitz.com/edit/qwik-starter is

export default defineConfig(() => {
  return {
    plugins: [qwikCity(), qwikVite(), tsconfigPaths()],
    preview: {
      headers: {
        'Cache-Control': 'public, max-age=600',
      },
    },
  };
});

The qwik plugins do some magic behind the scene while serving the site so you can not just use a file server as I suggested earlier.

I think what you want to do is:

  • use @nrwl/vite:build to vite build
  • use @nrwl/vite:build with the ssr option to vite build --ssr src/entry.preview.tsx
  • use (a modified version of) @nrwl/vite:preview-server to serve what you build in the first 2 steps.

I think this can look like:

    "preview": {
      "executor": "@nrwl/vite:preview-server",
      "defaultConfiguration": "development",
      "options": {
        "buildTarget": "qapp:build",
        "proxyConfig": "apps/qapp/proxy.conf.json"
        "build": false,
      },
      "configurations": {
        "development": {
          "buildTarget": "qapp:build:development"
        },
        "production": {
          "buildTarget": "qapp:build:production"
        }
      }
    },
    "qpreview": {
        "executor": "nx:run-commands",
        "options": {
            "commands": [
              "nx run qapp:build",
              "nx run qapp:build-srr",
              "nx run qapp:qpreview",
            ],
            "parallel": false
        }
    }

The only update to @nrwl/vite:preview-server is the additional "build" option.

"build" would default to true. That is we want to build before launching the preview server.

In your case you do not want to build so you would set that option to false and const build = await runExecutor(target, buildTargetOptions, context); would not be executed.

Does this sounds good?

@dmitry-stepanenko
Copy link
Contributor Author

dmitry-stepanenko commented Feb 8, 2023

Thanks @vicb. The setup I have now is pretty much the same to what you are suggesting with the only difference I'm relying on the "dependsOn" mechanism instead of an extra target.

Your solution should definetely solve the issue that I'm facing. And it fits very well in the existing case where we want the preview server to have the same options as the build target.
However, I see there a few questionable parts by looking at this solution without the context of my problem with qwik:

  • It doesn't seem that obvious why would we want to have build option to be ever set to false, since the whole point of preview is to serve built artifacts. Obviously I need to invoke another task between build and preview, but that's an edge case. Can you foresee any other similar scenarios?
  • We're still being bound in the necessity of using the combination of @nrwl/vite:build + @nrwl/vite:preview-server executor. What if someone would want to customize the build process? For example, as I mentioned before, I was considering wrapping two build steps into 1 using custom executor, which will definitely change the format of provided options so that they're not compatible with whatever preview server expects.

Don't you think that this approach https://github.com/nrwl/nx/compare/master...dmitry-stepanenko:ds/vite-preview-custom-build-target?expand=1 should provide more flexibility? Yes, we will loose the implicit combining of build and preview options, so that everything should be configured manually using configurations, but I don't think it's a big deal in the scenario where setup itself is not standard

@vicb
Copy link
Contributor

vicb commented Feb 8, 2023

vite preview only serves the built artifact but does not build them - you have to run vite build before.

The nrwl/vite:preview-server does 2 things:

  • build the artifact using the buildTarget which should point to a @nrwl/vite:build,
  • launch the preview server.

By default the watch mode is enabled and any app change will trigger a rebuild.

For you project (more generally when there is a need to customize the build step) we could have the preview step do only what vite preview does. That is serve serve only but skip the build step.

You have a good point when you say "build: false" could be confusing.

If I understand your changes correctly when you set customBuildTarget to true then the buildTarget must have outputPath option. I don't think it is very flexible or super easy to understand/explain.

IMO both our solutions solve the issue but not in the cleanest way.

So I am going to propose a 3rd solution:
What if:

  • we keep buildTarget but make it optional. When provided it must point to a @nrwl/vite:build otherwise the executor would not build but only serve,
  • we add an outputPath optional configuration option that must be set when buildTarget is not provided.
  • either buildTarget or outputPath must be provided.

This means that the default configuration would remains unchanged and the current preview continues building and serving.

When there is a custom build step, you would not provide the buildTarget and would take care of building your project via that custom build step. However you would add a outputPath pointing to the location of the build artifacts.

What do you think of that?

Some more details:

Today nrwl/vite:preview-server uses the buildTarget for 2 things:

  1. retrieve the outDir,
  2. launch the build before serving the built artifacts.

That's why even if we don't want to build (step 2) we still need outputPath (step 1).

I think it's a good discussion and we are converging to a good solution.

At this point I think it is safe to say that the current PR could be reverted. We can support your use case using a nicer solution. What do you think?

@vicb
Copy link
Contributor

vicb commented Feb 9, 2023

btw @dmitry-stepanenko maybe it's late to ask but I am not sure to understand what problem your initial CL was fixing (by looking at code and the description). Could you expand a little bit on that? That is what was not working?

@dmitry-stepanenko
Copy link
Contributor Author

dmitry-stepanenko commented Feb 9, 2023

Thank you @vicb. Your alternative 3rd solution is much more suitable for generic purposes. But to be honest, after implementing your solution I still think that mine with customBuildTarget is more concise 🙂 .

Mainly because of these reasons:

  • nx executors that are aimed to rely on the build artifacts always use "buildTarget" to produce those artifacts internally rather than expect them to be pre-built.
  • implementation with optional build step is a bit more messy, since we have to go through a lot of additional conditionals and validations
  • we're still locked into the need of providing path to the artifacts in both cases, but in my approach it can still be inferred from the build step, since "outputPath" is something that should always exist on such type of targets

Anyway, your solution should still be very convenient and understandable, I'm looking forward to proceeding with any of these 2. Let me know if you have any suggestion with its implementation, I hope I've done it according to your vision. Probably at this point it is worth letting @mandarini to decide 🙂

So Katerina, please let me know which of these 2 solutions you like more and whether you want me to make any changes to the one you pick (if any, of course):

@dmitry-stepanenko
Copy link
Contributor Author

btw @dmitry-stepanenko maybe it's late to ask but I am not sure to understand what problem your initial CL was fixing (by looking at code and the description). Could you expand a little bit on that? That is what was not working?

So the entire problem is that I need to invoke the ssr build between the client one and the preview server. Due to the order of execution I'm only able to specify the "build-ssr" as a "buildTarget" for the preview executor. Because of this preview server inferred wrong options from the build target. And the whole thing was solved by preventing the build options to be passed into the preview server

@mandarini
Copy link
Member

Thank you both for the detailed descriptions. I will revert the initial PR, and look at the two solutions suggested by @dmitry-stepanenko. I think that our preview-server should be consistent with the way vite preview works, in any case. I will discuss this with Jack, too, and come up with a solution by the end of the day.

@vicb
Copy link
Contributor

vicb commented Feb 9, 2023 via email

@mandarini
Copy link
Member

No rush at all! I just reverted that PR to make sure the original intended functionality remains the same. I will work on this tomorrow in any case. Thank you both very much

@vicb
Copy link
Contributor

vicb commented Feb 9, 2023

Long but super interesting discussion...

Having a solution that works does not mean it's a good solution. The solution should be understandable and explainable so that one can build a mental model of how to use it.

The mental model for nrwl/vite:preview-server is quite simple:

  • it first build the buildTarget,
  • and then launch a preview server.

It does not fit well the vite flow which is:

  1. vite build (config client)
  2. vite build --ssr src/entry.preview.tsx (config server)
  3. vite preview (config client)

The steps must be run in this order. You can not run 2 before 1.

So what @dmitry-stepanenko is trying to do is to fold 2 and 3 into a nrwl/vite:preview-server. It does not work because 2 and 3 use different configs (server vs client) and that's not how nrwl/vite:preview-server works (the executor uses the same config for build and serve which does not look unreasonable).

How do we reconcile all of that?

If we still think to nrwl/vite:preview-server as doing 1) build 2) serve then we can fold 1 and 2 (from the vite steps) into a build target (i.e. using nx:run-commands or even a custom qwik executor).

When nrwl/vite:preview-server see that the bulid target is not based on nrwl/vite:build it will not merge the options. It still needs the outputPath. It could try to retrieve it from the buildTarget and fallback to an option in the preview target configuration.

What do you think - It looks pretty clean to me.

Change details

We add one optional outputPath config option to nrwl/vite:preview-server.

When the executor runs it check what executor the build step uses.

If the build step uses nrwl/vite:build then we keep the current behavior (which is to use the same options for build and serve).

If the build step uses any other executor it makes no sense to merge the options. The buildTarget uses its own option as defined in the project.json. The preview target only needs to figure out what outputPath to use. It first check is the builtTarget has that option. If not it fallbacks to the the new outputPath option.

@dmitry-stepanenko
Copy link
Contributor Author

This makes sense @vicb ! Described above is quite similar to what I've done in my solution with customBuildTarget option. I've made a few changes there now to enrich the functionality according to your thoughts. That's definitely what was missing in it and it turns out to be nice indeed.

Here're the main points about the modified flow:

  • existing behavior is preserved
  • if customBuildTarget option is set to true, build and preview options will not be merged
  • if buildTarget's executor is not a @nrwl/vite:build, customBuildTarget will be set to true internally
  • outputPath can be specified explicitly on the preview target as staticFilePath or inferred from the build target. Executor throws if outputPath is not found in these 2 sources

The only thing I'm not sure about is customBuildTarget option name. Maybe something like mergeBuildTargetOptions will be more verbose and understandable? Because that's the only purpose of this option as of now.

@vicb
Copy link
Contributor

vicb commented Feb 10, 2023

I don't think we need the customBuildTarget option - the fact that you a have hard time finding a good name indicates that the concept is confusing. BTW mergeBuildTargetOptions would be even more confusing.

Otherwise I'm +1 on your CL. staticFilePath is great and matches other server configs.

@dmitry-stepanenko
Copy link
Contributor Author

But this again enforces me to switch to another executor to avoid merging of configs, since with @nrwl/vite:build we will always get them merged. I agree with your point that existing behavior should be present by default, but there might be other cases when people would like to avoid having both config merged.

This option is false by default and just brings some additional convenience with edge cases

@vicb
Copy link
Contributor

vicb commented Feb 10, 2023

In my opinion we don't need the customBuildTarget - let the Nx wizards decide.

In my opinion it's ok for Nx to solve 99% of the problems out of the box and leave aside the 1% custom configs especially when there are other way to do things.

You have different options for not having to use customBuildTarget:

  • call the preview vite preview command as it is done today,
  • create an executor for your build step (using nx:run-commands or custom)

@dmitry-stepanenko
Copy link
Contributor Author

Ok, Victor, that's great we at least have an agreement on the overall implementation, thank you for your help and ideas!

@mandarini I believe that we would want to move forward with this one master...dmitry-stepanenko:ds/vite-preview-custom-build-target, the only question is whether you would want to keep the customBuildTarget option in the schema (maybe with another name or as is?) or to remove it at all.

@mandarini
Copy link
Member

Ok, thank you both!!!! I will move forward with this on Monday! :D It's great that a conclusion was reached that works for all!

@mandarini
Copy link
Member

Regarding customBuildTarget, if it's not used or it's not needed much, I say we don't keep it. Simple is best!

@vicb
Copy link
Contributor

vicb commented Feb 10, 2023

Sounds goos to me.

@dmitry-stepanenko use case is very specific and IMO better addressed with a custom build/preview executor. If more use cases arise in the future we could still re-visit the decision.

I think @dmitry-stepanenko should open a PR with his current code as his solution is a good starting point.

@mandarini
Copy link
Member

Wow thank you both!!!! :D I just tested, and merged the updates!!!!!

@dmitry-stepanenko
Copy link
Contributor Author

Thanks @mandarini!

@github-actions
Copy link

github-actions bot commented Mar 3, 2023

This pull request has already been merged/closed. If you experience issues related to these changes, please open a new issue referencing this pull request.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 3, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants