The Genesis Global Community Success initiative is committed to open-sourcing select technologies that we believe the open-source community would benefit from.
When contributing to Custom Elements LSP Plugin (CEP), please follow the standards defined in this guide.
The first question you want to ask yourself when working on a CEP project is whether you need to change the core plugin itself, or whether you can write your own plugin which works with the exposed API. The API allows you to augment diagnostics, metadata, and completion info from the core. An example of a plugin is the cep-fast-plugin which is part of this monorepo. If you are not sure whether you are better writing a plugin or changing the core code, raise an issue on Github and ask.
If you are writing a plugin then see this section. Else, the next sections cover contributions to the monorepo.
To work with the project you'll need Git, and Node.js.
CEP uses Git as its source control system. If you haven't already installed it, you can download it here or if you prefer a GUI-based approach, try GitHub Desktop.
Once Git is installed, you'll also need Node.js, which CEP uses as its JavaScript runtime, enabling its build and test scripts. Node.js instructions and downloads for your preferred OS can be found here.
Other dependencies you'll need to use for working on CEP are installed via npm
, such as the TypeScript
compiler.
Now that your machine is setup, you can clone the CEP repository. Open a terminal and run this command:
git clone [email protected]:genesiscommunitysuccess/custom-elements-lsp.git
Cloning via HTTP:
git clone https://github.com/genesiscommunitysuccess/custom-elements-lsp.git
When working on the LSP codebase itself, it is useful to be running an application in order to see what functionality is and is not working. The app in packages/showcase/example
is setup to use the LSP plugin out of the box for NVIM and VSCode currently, other LSP IDEs may need some other configuration.
While developing:
npm run bootstrap
will do all required setup for installing and building packages for working on the LSP and using the test example apps. You should rerun this every time you change a dependency.npm run clean
can be used to clean up all of the nodes modules if you are running into issues. You'd need to then runnpm run bootstrap
again.npm run build:watch
from thecustom-elements-lsp
project root to incrementally transpile the plugin. You probably want this to be continually running while you're working in CEP. This is only rebuilding the CEP itself - if you are changing the code of theexample
app orexample-lib
lib then you'll either need to manually run their build commands, or run them vianpm run bootstrap
.
- Set an environment variable
TSS_LOG="-logToFile true -file /path/to/lsp_log.log -level verbose"
. Check however your environment or operating system requires environment variables to be set, as it may be different to this. - View logs at specified location in a text editor of your choice.
If you're using VSCode you can view the logs using
TypeScript: Open TS Server log
from the command palette.
The log file contains all of the output from the LSP itself, so it will contain a lot of additional output to the CEP output.
Logs are output from the CEP from logger.log()
commands, and these are prefixed with the token [CE]
. An example of this is:
Info 277 [13:15:45.841] [CE] diagnosticsUnknownTags: checkTags: invalid-component,banana
You can therefore search the file for [CE]
to filter for CEP output.
If you are not seeing any matches for
[CE]
but you're seeing the normal output from the LSP, then go towards the front of the file and see if you can find a stack trace or other error message. This is likely caused by a runtime error in the CEP during it initialising.
As mentioned previously, CEP contains an example application in the /packages/showcase/example
directory which can be used as a test project which is setup with the LSP plugin. The approach is based on https://github.com/orta/TypeScript-TSServer-Plugin-Template and https://github.com/microsoft/TypeScript/wiki/Writing-a-Language-Service-Plugin#overview-writing-a-simple-plugin.
example
application already contains all of the configuration required for use, as you can see in places like example/src/tsconfig.json
which contains configuration for the LSP as described in the readme.
The LSP plugin should be symlinked locally via npm workspaces, but if you're having issues you can manually link the plugin:
npm link
in the project rootcd example
npm link @genesiscommunitysuccess/custom-elements-lsp
The directory packages/showcase/example-lib
contains a small example library which publishes a FAST web component and associated custom elements manifest. It is added as a dependency of the example app already, and then you need to build the output using npm run build
for use in the example app. If you have run npm run bootstrap
from the project root then it will have build
While working on the LSP plugin you can test it out in any Typescript application you'd like, in addition to testing via the included /example
application.
- You need to install the plugin as a dev dependency to your application. You can do this either in a similar way to the
npm link
instructions in the previous section, or use thefile:
syntax for a local installation. - You need to set up your application as explained in this section. Again, you'll need to do any IDE specific setup such as configuring the
settings.json
of VSCode as explained there. - Run the
npm run bootstrap
andnpm run build:watch
commands in the LSP directory as explained previously.
Testing is currently only setup for unit tests, but we plan to add some e2e tests in the future too.
Testing for packages such as
@genesiscommunitysuccess/analyzer-import-alias-plugin
have their own contained tests.
{
"test:unit": "jest",
"test:unit:coverage": "jest --coverage",
"test:unit:debug": "node --inspect-brk node_modules/jest/bin/jest.js",
"test:unit:verbose": "cross-env TEST_LOG=1 jest",
"test:unit:watch": "jest --watchAll"
}
- We write test files in the same directory as the file it is testing, with the same name but a
.test.ts
suffix. - We write test infrastructure files in the
./src/jest/
directory. - We try and follow the Repository Design Pattern, and use stubbed versions of repositories where possible rather than using jest mocks.
- See
./src/plugin/custom-elements/repository.ts
and./src/jest/custom-elements.ts
for examples to understand this.
- See
- Unit testing a private function on a class is OK, by casting it
as any
. Doing this allows you to write more encapsulated unit tests. - We aim for a high test coverage, but don't put on a hard numerical rule in order to avoid writing useless unit tests just to pass a line count rule.
- Some unit tests require a shape of text input to test which goes against the normal linting rules. That is OK and in this case you should put the test case in
./src/jest/shaped-test-cases.ts
and follow the established pattern.
test:unit
, test:unit:coverage
and test:unit:watch
are simple wrappers around the jest commands. You can call these commands with a string at the end which will filter the tests. For example, if you want to watch unit tests from the strings.ts
utility file you can run npm run tset:unit:watch string
.
In the unit tests you won't see any logging from the CEP; however, if you use test:unit:verbose
you will see the logging in the tests.
If you are stuck with a unit test then you can use the node debugger to help. Use the test:unit:debug
command along with a debug
statement in your code.
You'll need a front end to use too; This only works with Chromium based browsers. Open your browser and go to chrome://inspect
and launch the debugger front end from there.
Note: you still filter the logging and debugging unit tests by filename too.
You can test CI workflows using act.
If you'd like to contribute by fixing a bug, implementing a feature, or even correcting typos in our documentation,
you'll want to submit a pull request. Before submitting a pull request, be sure to
rebase your branch (typically from master
, which is our
main branch). Please note we currently use Github Flow, merging strategy.
A few simple guidelines for release automation to run smoothly:
- Single commit representing each change making it easy to revert/cherry-pick if needed
- Clean Git history (no merge or WIP commits)
Commit message title is used in release notes. Commit message body isn’t included (it’s still visible in Git history). JIRA Ref, PR number and Git commit are clickable links providing more context.
PR Title should contain the ticket prefix as appropriate.
- Internal Genesis employees should be using
FUI-
as their ticket. - External contributions should link to an issue on GitHub, and use
ISSUE-
as their tickets.
- It’s critical that correct conventional commit category is specified, particularly flagging up any breaking changes.
- If in doubt, reach out to others
- If you’re a reviewer on a PR, double-check that it has the correct category
- As a rule of thumb:
- Bugfix: existing functionality fixed to behave correctly. User doesn’t need to do anything.
- Feature: new functionality. User needs to make changes to make use of the new APIs.
- Breaking change: user needs to perform steps to upgrade (they need to change an existing setting in
tsconfig.json
for example). - Chore: anything else. User doesn’t need to do anything.
- It’s from the point of view of public API / end-user
- If you introduce a bugfix/feature/breaking change, which isn’t exposed to the user in any way (either because it’s private API or it’s in supporting code such as test helpers), then it counts as a chore.
The master
branch is the default branch. It cannot be pushed to directly and will be pre-selected when you raise a PR.
We do not use a develop branch. master
is used for all releases pinned to the current major version. Pull requests
should be merged directly into this branch so that they can be automatically checked for publishing.
To contribute a feature, bugfix etc, please create a working branch off master using your Github username and some contextual detail. For example:
The format should be <github-user>/<ticket-name>-<short-description>
.
git checkout master
git pull
git checkout -b matteematt/issue-2-fix-diagnostics
As explained in the PR title section, ticket names are dependent on whether you are an internal or external contributor.
We follow conventional commits in CEP, and have commit linting in place when you attempt to push. Please do not bypass these hooks as your PR will be rejected by reviewers. If you are unfamiliar with conventional commits please read the conventional commits spec and see the project's git log for examples.
You can validate any commit message using commitlint
task (including multiline messages):
> npm run commitlint fix: Desc
⧗ input: fix: Desc
✖ subject must not be sentence-case, start-case, pascal-case, upper-case [subject-case]
✖ references may not be empty [references-empty]
✖ found 2 problems, 0 warnings
Fix issues and re-run:
> npm run commitlint fix: desc ISSUE-1
⧗ input: fix: desc ISSUE-1
✔ Commitlint check successful``
Our commitlint configuration is setup to use ticket prefixes are required.
Here's an example of a fix, which correlates with PATCH in Semantic Versioning:
fix: attribute diagnostics where attributes are substrings of others FUI-1416
Here's an example of a feature added, which correlates with MINOR in Semantic Versioning:
feat: diagnostics support for non-custom elements FUI-1227
Here's an example of a breaking change with a verbose commit message. It's a feature with a breaking change as indicated by a footer for extra visibility. This correlates with MAJOR in Semantic Versioning:
feat: continuously analyse source code to update custom elements manifest ISSUE-3
BREAKING CHANGE: Changed required config in tsconfig.json to set src and dependencies config in a parser block.
If you are addressing multiple issues which are unrelated, consider either doing multiple pull requests, or doing separate scoped commits to ensure accurate generation of changelogs and versioning of packages.
IMPORTANT: If you are finding that your changes are either breaking changes or require multiple pull requests, open a discussion to discuss this. We want to avoid breaking changes.
All pull requests should use the primary commit message as the title and our PR template which is added to the PR by default. Please add the ticket number to the end of the PR title if not present. Please complete all the relevant sections to the best of your ability as failure to do so may result in PR rejection.
For the time being, only core CEP maintainers will have the necessary security privileges to merge pull requests. When merging a branch to the default master
branch, changes should be squashed:
It is critical to retain all the conventional commit information, as some of these commit messages may detail breaking changes etc, the severity of which will dictate the Semantic Versioning required for the package.
An essential consideration in every pull request is its impact on the system. To manage impacts, we work collectively to ensure that we do not introduce unnecessary breaking changes, performance or functional regressions, or negative impacts on usability for users or supported partners.
This section covers some basic information for how to write a CEP Plugin. An example of this is the cep-fast-plugin which you can download and use as a template for your own plugin.
The basic premise is that your project must expose a function which is of the TypeScript type CEPPlugin
, and it is the default export.
import { CEPPlugin } from '@genesiscommunitysuccess/custom-elements-lsp/out/main/plugins/export-interface';
const myPlugin: CEPPlugin = (logger, services, userConfig) => {
logger.log('my plugin loaded');
return { }; // This plugin would do nothing except log "my plugin loaded" upon initialisation
};
export default myPlugin;
CEPPlugin
is a function which takes logger
, services
, userConfig
and returns an object which can provide optional enhancements to completions
, diagnostics
, and metadata
(e.g. quickinfo). If you look at index.ts
in the cep-fast-plugin
you can see that it is providing enhancements for all three sections, but you may only augment one or two of them if that is all you require.
The process then works like a pipeline.
- The user performs an action in their IDE.
tsserver
generates a base LSP message.- This is run in the CEP and there is some base functionality which happens.
- Any registered plugins will then have their applicable methods called, allowing them to augment the message.
- The message is returned back to
tsserver
and the appropriate action will be handled via the user's IDE.
The order in which plugins are specified in the
plugins
config in the CEP config block in the user'stsconfig.json
is the same order that plugins will be applied in step 4.
To explain the above 5 points with an example, consider the user installing the cep-fast-plugin
and having the following template which should be valid
<my-element :attr=${(x) => x.foo}></my-element>
- Diagnostic actions are triggered by the IDE usually when a code change occurs in a file/buffer.
tsserver
generates an information message.- The base CEP sees the property binding syntax
:attr
, but because that is FAST specific it just treats that as an invalid attribute. It will therefore generate an error diagnostic. - The
cep-fast-plugin
diagnostics function knows how to handle property binding syntax, so it verifies that<my-element>
has that as a valid property. It therefore removes that diagnostic from the pipeline. - The message is returned to the IDE. Because step 4 removed it (because in this case it was valid) then the IDE should report no diagnostic issues.
As well as the API interface that is exposed via the logger
and services
(as well as the interface that you can implement from typescript to actually have the required functions on your class), there are more things that the CEP expose for you.
You can import all of these from the single import point
import { utils, CONSTANTS, Services, /* and more */ } from '@genesiscommunitysuccess/custom-elements-lsp/out/main/plugins/export-interface';
You can look at the declaration file from that import to see what is exposed. Some of the things which could be of interest.
- Access to the
kvstore
to work with the cache. - Access to constants, such as what type of diagnostic issue something is.
- Access to functions for parsing and manipulating strings.
- Access to functions which can be used to construct fakes for unit tests.
The cep-fast-plugin
contains examples of using all of these different imports for you to look at. As the project is open source you can always look at the implementation in the main CEP project too.
The import large import path is cumbersome, but we have to use subpaths because of a limitation with
tsserver
and how it needs to import the main CEP from its package.