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(ui-devkit): Support module path mappings for UI extensions #1994

Merged

Conversation

vrosa
Copy link
Contributor

@vrosa vrosa commented Jan 18, 2023

Context

Given the composable and extensible nature of Vendure, a monorepo codebase where each package encapsulates a custom plugin is most likely a common practice. Often times, different plugins that include Admin UI extensions share similar behaviour/UX and could depend on the same set of custom Angular components, services, etc. However, considering how Admin UI extensions are compiled - the extensionPath directory gets copied as is to the output and is "self-contained" so to speak - referencing code outside the folder and, more importantly, modules declared in other extensions is tricky and unintuitive. Sure, you can give ids to AdminUiExtensions and use relative imports but that requires knowledge of the inner workings of ui-devkit and is a brittle approach as the implementation details might change at some point, rendering the solution obsolete (and broken). Additionally, relative paths make sense in the context of the ui-devkit build process but not to the project's main TypeScript configuration since the path will always be invalid. As a result, IDE features such as code completion and quick navigation, as well as linting do not work. An alternative is to create an NPM package for the common module, but besides all the hassle (build/packaging process, private NPM registry, CI, etc) it doesn't give you the same developer experience where changes can be immediately verified.

Proposed solution

Allow developers to specify module path mapping for UI extensions so they are able to import code from other bundled extensions, while at the same time maintaining compatibility with the project's main tsconfig.json.

Description of changes

  • Update @vendure/ui-devkit to support a new pathAlias property in AdminUiExtension objects, which represents an alias for the module so it can be referenced in import statements from other UI extensions. The path is resolved relative to the extensions directory of the compiled admin-ui folder, whose children are every declared UI extension. A full example has been added as a docstring.
  • Change scaffold.ts to check if any extensions have pathAlias defined; if that's the case, update the paths property of the bundled tsconfig.json file to map each pathAlias to its corresponding compiled folder. Since the attribute is resolved relative to baseUrl - the root of the compiled admin-ui folder - each path is prepended with src/extensions/.
  • Minor tweaks to existing docs and removal of an unused import from scaffold.ts.

Notes

  • Development and testing were executed using this approach
  • I really hope the feature gets accepted as I've already patch packaged Vendure in our project because we desperately need this! 😅

const tsconfigFilePath = path.join(outputPath, 'tsconfig.json');
const indexFilePath = path.join(outputPath, '/src/index.html');
if (fs.existsSync(tsconfigFilePath) && fs.existsSync(indexFilePath)) {
addModulePathMappingIfDefined(tsconfigFilePath, modulePathMapping);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because the mapping is not a file and cannot be watched, we need to ensure it gets updated every time the local server starts again/a file is changed.

@michaelbromley
Copy link
Member

This is an excellent addition and an overall great PR as usual :) This exact issue has come up a few times, so thanks for coming up with a solution.

Before merging, I've got an idea I'd like to explore. Would it be possible to, instead of defining a new kind of extension just for the mapping, to include its functionality in the existing AdminUiExtension? So it looks like this:

export const uiExtensions: AdminUiExtension = {
  id: 'common-ui',     // this is important
  modulePathMapping: { 
    '@common-module/ui': 'common-ui/ui-shared.module.ts',
  },
  extensionPath: path.join(__dirname, 'ui'),
  ngModules: [
    {
      type: 'shared' as const,
      ngModuleFileName: 'ui-shared.module.ts',
      ngModuleName: 'CommonSharedUiModule',
    },
  ],
};

Advantages are

  1. Everything is co-located, so e.g. a change to the id makes it harder to forget the corresponding change needed to the path mapping.
  2. Just need to import the uiExtensions object, no need to then go and define a separate object just for the mapping.

Actually I hard a further thought:

  • Could we actually make the mapping "automatic"? In the extension definition, we already know the id, so we can therefore infer the path relative to the compiled admin-ui dir. Furthermore we know the module filename since it is given in ngModuleFileName. So perhaps we could have a syntax like:
export const uiExtensions: AdminUiExtension = {
  id: 'common-ui',     // this is important
  extensionPath: path.join(__dirname, 'ui'),
  ngModules: [
    {
      type: 'shared' as const,
      ngModuleFileName: 'ui-shared.module.ts',
      ngModuleName: 'CommonSharedUiModule',
      pathMapping: '@common-module/ui',
    },
  ],
};

What do you think? Are either of those doable?

@vrosa vrosa force-pushed the feat/support-ts-path-mappings branch from 59d67b8 to d266c61 Compare January 19, 2023 06:44
@vrosa
Copy link
Contributor Author

vrosa commented Jan 19, 2023

@michaelbromley Those were great suggestions, I also prefer everything configured in only one place. I just went one step further and ditched the need for an explicit id: if not provided, we generate our own and therefore still know the compiled folder name. All the user has to do is provide a pathAlias for the AdminUiExtension and the mapping happens behind the scenes. I chose to do it at the extension level - as opposed to NgModules - so the developer is free to import from any file inside the extensionPath directory, including an index file if there is one.

I've updated the PR description accordingly.

@@ -280,3 +368,7 @@ export interface BrandingOptions {
largeLogoPath?: string;
faviconPath?: string;
}

export interface AdminUiExtensionWithId extends AdminUiExtension {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not very elegant but that was the only way I found to make id required but not pathAlias.

Copy link
Member

Choose a reason for hiding this comment

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

It's fine - it communicates the type and it's internal anyway.

Copy link
Member

@michaelbromley michaelbromley left a comment

Choose a reason for hiding this comment

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

I absolutely love it! Great work!

One small thing to change on the docs, then we're good to go!

@@ -280,3 +368,7 @@ export interface BrandingOptions {
largeLogoPath?: string;
faviconPath?: string;
}

export interface AdminUiExtensionWithId extends AdminUiExtension {
Copy link
Member

Choose a reason for hiding this comment

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

It's fine - it communicates the type and it's internal anyway.

* @example
* ```ts
* // packages/common-ui-module/src/ui/ui-shared.module.ts
* import { NgModule } from '@angular/core';
Copy link
Member

Choose a reason for hiding this comment

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

In order to not break the docs, every @ character in a code block needs to be escaped.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done! Is there a way to check the final docs as they appear on the website? I can see the md file generated after running yarn docs:generate-typescript-docs locally but I don't think I have the right tool to visualise it.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks!
Unfortunately not at the moment. I plan to move the doc gen stuff back into the monorepo soon when we update our website.

@michaelbromley michaelbromley merged commit 6d57c86 into vendure-ecommerce:minor Jan 19, 2023
@michaelbromley
Copy link
Member

🥳

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.

2 participants