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!
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:
- they define one or more
Project
instances - these projects are configured and customized
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.
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):
- the project's
preSynthesize()
method is called - all components'
preSynthesize()
methods are called - all projen-synthesized files are cleaned up
- all components'
synthesize()
methods are called (most files are generated) - all components'
postSynthesize()
methods are called - 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
andTextFile
), 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.)
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.