Skip to content

Latest commit

 

History

History
339 lines (227 loc) · 15.6 KB

File metadata and controls

339 lines (227 loc) · 15.6 KB

Contributing

Welcome!

We're excited for your interest in Greenwood, and maybe even your contribution!

We encourage all contributors to first read about the project's vision and motivation on our website.

Setup

To develop for the project, you'll want to follow these steps:

  1. Install NodeJS LTS or NVM (recommended)
  2. Have Yarn 1.x installed
  3. Clone the repository
  4. For NVM users, run nvm use
  5. Run yarn install

Feature Development

Patch Package

Generally we prefer to develop new features in the context of a project, working directly within node_modules and validating the behavior or fix first hand. Since Greenwood runs on plugins, a lot can often be achieved by just creating a custom plugin in a project's greenwood.config.js file.

If changes to node_modules are needed, use patch-package to create a snapshot of those changes and provide that repo and patch along with your PR.

Testing

Test Cases

Greenwood relies on a large set of test suites that are very behavior based, in that we can scaffold out a full Greenwood project, including a greenwood.config.js and run any of Greenwood's commands over the project files. Combined with mocha for testing and gallinago for running Greenwood commands, any combination of configuration, project structure, and Greenwood command can be tested for its output. (in other words, we favor E2E / BDD testing, as opposed to unit testing).

Here an example test case:

import chai from 'chai';
import { JSDOM } from 'jsdom';
import path from 'node:path';
import { runSmokeTest } from '../../../../../test/smoke-test.js';
import { getOutputTeardownFiles } from '../../../../../test/utils.js';
import { Runner } from 'gallinago';
import { fileURLToPath, URL } from 'node:url';

const expect = chai.expect;

describe('Build Greenwood With: ', function() {
  const LABEL = 'Default Greenwood Configuration and Workspace';
  const cliPath = path.join(process.cwd(), 'packages/cli/src/index.js');
  const outputPath = fileURLToPath(new URL('.', import.meta.url));
  let runner;

  before(function() {
    this.context = {
      publicDir: path.join(outputPath, 'public')
    };
    runner = new Runner();
  });

  describe(LABEL, function() {

    before(async function() {
      await runner.setup(outputPath);
      await runner.runCommand(cliPath, 'build');
    });

    runSmokeTest(['public', 'index'], LABEL);

    describe('Default output for index.html', function() {
      let dom;

      before(async function() {
        dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, './index.html'));
      });

      describe('default <head> section content', function() {
        it('should have a <title> tag in the <head>', function() {
          const title = dom.window.document.querySelector('head title').textContent;

          expect(title).to.be.equal('My App');
        });

        // ...
      });
    });
  });

  after(async function() {
    await runner.teardown(getOutputTeardownFiles(outputPath));
  });
});

Running Tests

To run tests in watch mode, use:

$ yarn test:tdd

To verify compliance with coverage and watermark thresholds (what CI server runs), use:

$ yarn test
$ yarn test:loaders

Below are some tips to help with running / debugging tests:

  • describe.only / it.only: only runs this block
  • xdescribe / xit: don't run this block
  • Uncomment await runner.teardown() in a case to see the build output without it getting cleaned up post test run
  • Use new Runner(true) get debug output from Greenwood when running tests

PLEASE DO NOT COMMIT ANY OF THESE ABOVE CHANGES THOUGH

Code Content Testing

In some cases tests may actually check for specific build output contents to confirm certain operations like custom bundling or linking operations within the Greenwood build process worked as expected. Keep in mind that if you change these contents as part of a test, and then Prettier formatting is run, the results may change and the test cases may fail, so just make sure to double check these contents with formatting applied first.

Writing Tests

Test cases follow a convention starting with the command (e.g. build) and and the capability and features being tested, like configuration with a particular option (e.g. port):

<command>.<capability>.<feature>-<modifier>.spec.js

Examples:

  • build.default.spec.js - Would test greenwood build with no config and no workspace.
  • build.config.workspace-custom.spec.js - Would test greenwood build with a config that had a custom workspace
  • build.config.workspace-dev-server-port.spec.js - Would test greenwood build with a config that had a custom workspace and devServer.port set.

Custom Loaders

Test cases that exercise custom loaders (like TypeScript, JSX plugins) for SSR and prerendering use cases, will need to do a couple things:

  1. Prefix the test case directory and spec file with loaders-
  2. Make sure to pass true as the second param to Runner
    import { Runner } from 'gallinago';
    let runner;
    
    before(function() {
      // pass true as the second param here
      runner = new Runner(false, true);
    });
    
    await runner.runCommand(/* ... */);
  3. Use the yarn test:loaders npm script

Notes

Here are some things to keep in mind while writing your tests, due to the asynchronous nature of Greenwood:

  • Make sure to wrap all calls to TestBed with async
  • All usages of JSDOM should be wrapped in async
  • Avoid arrow functions in mocha tests (e.g. () => ) as this can cause unexpected behaviors.. Just use function instead.

Dependencies

To add and remove packages for any workspace, make sure you cd into the directory with the package.json first before running yarn add or yarn remove.

For example:

$ cd packages/cli
$ yarn add <package>

Yarn workspaces will automatically handle installing node_modules in the appropriate directory.

Continuous Integration

Greenwood makes active use GitHub Actions and Netlify deploy previews as part of the workflow. Each time a PR is opened, a sequence of build steps defined .github/workflows/ci.yml are run for Linux and Windows including running tests, linting, and formatting.

A deploy preview is also made available within the status checks section of the PR in GitHub and can be used to validate work in a live environment before having to merge.

Types

Greenwood provides types for a number of its key primitives (configuration, plugins, content as data) as well as for all plugins as well as JSDoc annotations where applicable. It is important to keep in mind to updates these as features are developed and iterated upon.

Additionally, Greenwood leverages exports maps as part of its distribution through NPM, which means (generally) every plugin should only have main and exports defined, in this convention:

{
  "type": "module",
  "main": "./src/index.js",
  "exports": {
    ".": {
      "types": "./src/types/index.d.ts",
      "import": "./src/index.js"
    }
  }
}

Each plugin will also need to have an index.d.ts file that exports types and a module definition for itself, like so:

// import the most specific plugin type relative to what your plugin uses
import type { Plugin } from "@greenwood/cli";

type SUPPORTED_THING = "A" | "B" | "C";

type FooPluginOptions = {
  bar?: SUPPORTED_THING
};

export type FooPlugin = (options?: FooPluginOptions) => [Plugin];

declare module "@greenwood/plugin-foo" {
  export const greenwoodPluginFoo: FooPlugin;
}

Technical Design

The Greenwood repo is a combination of Yarn workspaces and a Lerna monorepo. The root level package.json defines the workspaces and shared tooling used throughout the project, like for linting, testing, etc.

The main workspace is the packages/ directory, which is everything we publish to NPM under the @greenwood scope.

This guide is mainly intended to walk through the cli package; it being the principal package within the project supporting all other packages. See our website for documentation on our Plugin APIs.

CLI

The CLI is the main entry point for Greenwood, similar to how the front-controller pattern works. When users run a command like greenwood build, they are effectively invoking the file src/index.js within the @greenwood/cli package.

At a high level, this is how a command goes through the CLI:

  1. Each documented command a user can run maps to a script in the commands/ directory.
  2. Each command can invoke any number of lifecycles from the lifecycles/ directory.
  3. Lifecycles capture specific steps needed to build a site, serve it, generate a content dependency graph, etc.

Package Structure

The structure of the CLI package is as follows:

  • index.js - Entry point for the CLI
  • commands/ - map to runnable userland commands
  • config/ - Tooling configuration
  • data/ - Content as data related functionality
  • lib/ - Custom utility and client facing files
  • lifecycles/ - Individual tasks that can be used by commands to support a full Greenwood lifecycle
  • plugins/ - Greenwood plugins maintained by the CLI project
  • layouts/ - Default layouts and / or pages provided by Greenwood

Lifecycles

Aside from the config and graph lifecycles, all lifecycles (and config files and plugins) typically expect a compilation object to be passed in.

Lifecycle responsibilities include:

  • starting a production or development server for a compilation
  • optimizing a compilation for production
  • prerendering a compilation for production
  • fetching external (content) data sources

Project Management

We take advantage of quite a few features on GitHub to assist in tracking issues, bugs, ideas and more for the project. We feel that being organized not only helps the team in planning out priorities and ownership, it's also a great way to add visibility and transparency to those following the project.

Project Boards

Our sequentially named project boards help us organize work into buckets that will generally include a small handful of "top line" goals and objectives we would like to focus on for that particular phase of work. It also serves as a catch-all for the usual work and bug fixes that happens throughout general maintenance of the project. Additionally, we leverage this as a means to shine insight into good opportunities for those interested in contributing as to what the Greenwood team would appreciate help with the most.

Discussions

We believe good collaboration starts with good communication. As with most of the open source community, Greenwood is a 100% volunteer project and so we understand the importance of respecting everyone's time and expectations when it comes to contributing and investing in a project. Although we don't mind issues being made, unless the issue is clearly actionable and falls in-line with the motivations and trajectory of the project, then feel free to go ahead an open a Discussion first.

We encourage discussions as we believe it is better to hash out technical discussions and proposals ahead of time since coding and reviewing PRs are very time consuming activities. As maintainers, we want to make sure everyone gets the time they are desire for contributing and this this workflow helps us plan our time in advance to best ensure a smooth flow of contributions through the project.

Put another way, we like to think of this approach as measuring twice, cut once.

Issues

We like to reserve issues for features and requests that are more or less "shovel" ready with clear implementation details at hand and a clear definition of "done". This could include prior discussions with the team or action items coming out from an existing discussion.

Our standard issue template requests some of the following information to be prepared (where applicable)

  1. High Level Overview
  2. Code Sample or API Design
  3. Links / references for more context

Pull Requests

Pull requests are the best! To best help facilitate contributions to the project, we have Conventional Commits configured for the project to walk you through preparing commits in the format of <type>(<scope>): #<issue> <summary of change>, e.g. bug(cli): #123 fixed bug with the thing.

Make sure you have run yarn lint, yarn format and yarn test to prepare your commit.

Then, after staging your files with git add, you can initiate the commit "wizard" by running:

$ yarn commit

The following will be required:

  • type
  • scope
  • issue reference (can technically be empty)

⚠️ Note: The breaking change prompt / option is broken in commitlint; [1], [2]. Please call out breaking changes in your PR.

To test the CI build scripts locally, run the commands mentioned in the Continuous Integration section of this document. (basically just make sure linting, formatting, and test tasks are all passing).

Release Management

Lerna (specifically lerna publish) will be used to release all packages under a single version bump. Lerna configuration can be found in lerna.json at the root of the repo. All packages are managed using Yarn (1.x) workspaces.

Assuming you are logged into npm locally and have 2FA access to publish, the following workflows should be used. Lerna should then prompt you through the steps to pick the version and all packages that will get updated.

Dry Run

To test Lerna's publishing output to see what changes it would make, you can run Lerna in "dry run" mode using the following command:

🚨 !!! Make sure to cancel (Ctr+C) in the terminal when prompted with the OTP prompt for npm publishing !!!

# from the root of the repo
$ yarn lerna publish --force-publish --no-git-tag-version --no-push

Alpha (Pre) Release

When working on a new minor release line, releases will be cut with an -alpha.N suffix / tag, e.g. v0.33.0-alpha.1. This ensures that new release lines can be tested without impacting what is tagged as latest in NPM, leveraging NPM's concept of dist tags.

To generate an alpha release, run:

# from the root of the repo
$ yarn lerna publish --force-publish --dist-tag alpha

Typically you will want to select the Custom Preminor option from the list, which Lerna should appropriately yield the expected version. But double check and make sure the version bump is correct.

Standard Release

For a formal release, e.g. latest, run the following command:

# from the root of the repo
$ yarn lerna publish --force-publish