Skip to content

Latest commit

 

History

History
99 lines (77 loc) · 4.82 KB

ARCHITECTURE.md

File metadata and controls

99 lines (77 loc) · 4.82 KB

Architecture

This document attempts to document the high-level architecture of projen. This could be useful if you're trying to contribute to projen, trying to debug an error message, or if you're just curious!

How are a project's files synthesized?

Bird's eye view

When npx projen is run, the command-line process executes the project's projenrc file. This is usually a file like .projenrc.js or projenrc.java.

The "rc" in the name is a common convention for configuration files - see https://en.wikipedia.org/wiki/Configuration_file.

projenrc files follow a general structure:

  1. they define one or more Project instances
  2. these projects are configured and customized
  3. project.synth() is called on the root project

For simplicity, most of this document will just assume there is a single project unless otherwise specified.

Steps 1 and 2 only serve to initialize an in-memory representation of the project. projen runs on Node.js; so in the JavaScript runtime, these steps create a hierarchy of objects (called Component's), each with various fields specifying the names of files, tasks, options, and so on. The data within each component provides enough information to uniquely determine the structure and contents of the files it is responsible for. Components can add other components to the project, and even make changes to existing components through common interfaces like project.tasks, project.deps, or project.tryFindObjectFile().

Step 3 is the only step that actually performs any changes to files in the user's project / file system.

Synthesizing actual files

The synth() method of Project performs the actual synthesizing (and updating) of all configuration files managed by projen. This is achieved by deleting all projen-managed files (if there are any), and then re-synthesizing them based on the latest configuration specified by the user. In code, this breaks down as follows (slightly simplified):

  1. the project's preSynthesize() method is called
  2. all components' preSynthesize() methods are called
  3. all projen-synthesized files are cleaned up
  4. all components' synthesize() methods are called (most files are generated)
  5. all components' postSynthesize() methods are called
  6. the project's postSynthesize() method is called

In the above list, step 3 is critical since it's important that only files that are managed by projen get cleaned up - we don't want user source code to be deleted! Moreover, if a file was synthesized by projen at one point in time, but later a user changes their projenrc configuration so it is no longer necessary, we want it to be automatically cleaned up.

Rather than manually keeping track of synthesized files with some form of stored state (which could easily get desynced by tampering from users or other tools), projen simply looks for files with the magic string that you get by concatenating "~~ Generated by " and "projen", and removes them. See cleanup.ts.

Since any file with this string gets automatically cleaned up, you should not include this magic string verbatim in source code files. If you are writing your own projen project type or component, you can simply reference this magic string via FileBase.PROJEN_MARKER.


Steps 1, 2, 4, 5, and 6 are more straightforward. synthesize() is used to generate the actual files in the user's file system (including applying appropriate read/write permissions). preSynthesize() and postSynthesize() are complementary methods that can used to enable components to perform additional logic before and after synthesis. See the source code of Component and FileBase for more details.

Note: in practice, there are many existing components for creating specific types of files (such as JsonFile and TextFile), so we recommend using these over hand-making components wherever possible. (Believe in the power of abstractions!)

Since preSynthesize() is called before any files are cleaned up, it can be used for e.g. observing any changes made to a generated file, and then adjusting how the file is re-synthesized based on those changes. (As an example, running npm install or yarn install can change the dependencies listed in the package.json file of JavaScript projects. The built-in NodeProject uses preSynthesize() to automatically integrate these changes to the package.json file synthesized by projen, instead of overriding them.)

How can projenrc files be written in multiple languages?

The projen library is transpiled by jsii so that projenrc files can be written in languages besides JavaScript. Under the hood, API calls made in projen's Java/Python/etc. libraries communicate with a JavaScript runtime to deliver the same behavior as if you wrote the code in JavaScript. For more information, check out jsii.