Skip to content

Commit

Permalink
fix(config): fix faulty build output w/ --esm flag (#3404)
Browse files Browse the repository at this point in the history
Stencil v2.16.0 introduced a regression, the symptoms of which are
described here: #3403. Git
bisect showed that this commit was the culprit:
cca8951.

What was the regression?
------------------------

What the user observed is that after upgrading from Stencil version
2.15.2 to version 2.16.0 the build output changed. With 2.15.2, when
calling stencil like so:

```
stencil build --docs --dev --esm
```

the build output looked something like this:

```
dist
├── cjs
├── collection
│   ├── components
│   ├── test
│   └── utils
├── esm
│   └── polyfills
├── loader
├── stencil-one
└── types
```

Whereas after upgrading to 2.16.0 the output looked something like this:

```
dist
├── stencil-one
└── types
```

Why did the built output change?
--------------------------------

Stencil previously explicitly supported a `--esm` boolean command-line
flag. Before [the change to remove the legacy
compiler](63595bc)
this flag was used to control whether the output would be ES modules or
not (I _think_).

The flag was removed from the supported arguments list in [that same
commit](63595bc#diff-e6a96bc13cd5c047a16a1888b27e88af52903e89848de68b0072d32ea11b1208L196),
however some code that implicitly depended on the argument remained in
the codebase, and it was eventually (partially) [added back to the
codebase later on](#2339) but,
although a change was made to `src/cli/parse-flags.ts` to support
parsing the flag, it was *not* added to the `ConfigFlags` interface,
which is intended to capture the names and types of all CLI arguments.

Related to this history of the flag being added and removed from
Stencil, on `src/compiler/config/validate-config.ts` there was, prior to
the bad commit which introduced this regression, a call to the
`setBooleanConfig` helper that looked like this:

```ts
setBooleanConfig(config, 'buildDist', 'esm', !config.devMode || config.buildEs5);
```

This line creates an undocumented dependency on the `--esm` command-line
flag, where it's presence or absence changes what the value of
`config.buildDist` will be.

This happens because of the implementation of `setBooleanConfig`, where
if a value is passed for the third positional argument it will look at
the command-line arguments passed to the Stencil CLI in order to
find a value to set on the `Config` object. The relevant code looks
like this:

```ts
export const setBooleanConfig = <K extends keyof d.Config>(
  config: d.UnvalidatedConfig,
  configName: (K & keyof d.ConfigFlags) | K,
  flagName: keyof d.ConfigFlags | null,
  defaultValue: d.Config[K]
) => {
  if (flagName) {
    const flagValue = config.flags?.[flagName];
    if (isBoolean(flagValue)) {
      config[configName] = flagValue;
    }
  }
```

The [commit that introduced the
regression](cca8951#diff-53c120bef3205f89229fbe98a5aadfcb51012ead144729ade2fc597c1e9ad5e9R64)
changed the above call to `setBooleanConfig` to this:

```ts
setBooleanConfig(config, 'buildDist', null, !config.devMode || config.buildEs5);
```

The user observed the regression because they were using the `--esm`
command-line flag with their project, so on any stencil version <=
2.15.2 the presence of that flag was causing `config.buildDist` to be
set to `true`, resulting in the expected build output above. In 2.16.0
the behavior of the `--esm` flag was broken by change from `"esm"` to
`null`.

Why was the change introduced?
------------------------------

This change was introduced because `"esm"` was not listed as a supported
command-line argument on the `ConfigFlags` interface and the third
position argument to `setBooleanConfig` has type `keyof d.ConfigFlags`,
so the change to `null` was in line with what `setBooleanConfig` expects
for its arguments. The commit author (me!) was unsure of whether to make
this change or not, and there was [some discussion on the
PR](#3335 (comment)).
Light testing did not turn up the regression because we weren't
using the `--esm` flag.

What is the fix?
----------------

The fix is to add `esm` to the `ConfigFlags` interface and to revert the
bad line in `src/compiler/config/validate-config.ts`. This will allow
anyone who is currently using the `--esm` flag to keep doing so.

This fix does not deal with whether or not we want to continue
supporting the `--esm` flag — it's possible that a larger refactor to
how the `buildDist` variable is used throughout the codebase is in order
— but doing this will unblock any users who are currently depending on
Stencil's previous, undocumented behavior.

I think this is the right fix for now, however, because an API change
like this should not happen on a minor release, so the behavior of the
compiler changing in this way should be viewed as a bug and we should
take the steps to restore the old behavior.

Any larger lessons learned?
---------------------------

Part of the reason for the regression was that the CLI argument parsing
code does not have a type level contract with the interface into which
the CLI args are parsed. In particular, there are lists of supported CLI
flags in `src/cli/parse-flags.ts` but these have _no connection_ with
the `ConfigFlags` interface, which describes the shape of the object
into which they are going to be shoved after their values are parsed.

This means that although we have an interface that sort of _claims_, at
least implicitly, because of how it's named, to enumerate the possible /
allowed CLI arguments, we aren't actually utilizing the type system to
enforce a tighter contract between the parsing code and that interface,
so there is a possibility of the arguments declared in one or the other
of these two places (the `parse-flags.ts` code or the `ConfigFlags`
interface) drifting from the other.

To address that root cause I think we should implement something to
declare these properties in one place. For instance, we could declare a
`ReadonlyArray` of Boolean flags, another of String flags, and then use
types derived from those as keys for the `ConfigFlags` argument.

Additionally, several of the commits in the history of the repository
which had to do with this flag have no discussion, documentation, or
context provided for the changes.
  • Loading branch information
alicewriteswrongs committed Jun 3, 2022
1 parent 30dc745 commit a847a6e
Show file tree
Hide file tree
Showing 3 changed files with 23 additions and 1 deletion.
21 changes: 21 additions & 0 deletions src/compiler/config/test/validate-config.spec.ts
Expand Up @@ -401,4 +401,25 @@ describe('validation', () => {
expect(config.sourceMap).toBe(false);
});
});

describe('buildDist', () => {
it.each([true, false])('should set the field based on the config flag (%p)', (flag) => {
userConfig.flags = { esm: flag };
const { config } = validateConfig(userConfig);
expect(config.buildDist).toBe(flag);
});

it.each([true, false])('should fallback to !devMode', (devMode) => {
userConfig.devMode = devMode;
const { config } = validateConfig(userConfig);
expect(config.buildDist).toBe(!devMode);
});

it.each([true, false])('should fallback to buildEs5 in devMode', (buildEs5) => {
userConfig.devMode = true;
userConfig.buildEs5 = buildEs5;
const { config } = validateConfig(userConfig);
expect(config.buildDist).toBe(config.buildEs5);
});
});
});
2 changes: 1 addition & 1 deletion src/compiler/config/validate-config.ts
Expand Up @@ -61,7 +61,7 @@ export const validateConfig = (
setBooleanConfig(config, 'sourceMap', null, typeof config.sourceMap === 'undefined' ? false : config.sourceMap);
setBooleanConfig(config, 'watch', 'watch', false);
setBooleanConfig(config, 'buildDocs', 'docs', !config.devMode);
setBooleanConfig(config, 'buildDist', null, !config.devMode || config.buildEs5);
setBooleanConfig(config, 'buildDist', 'esm', !config.devMode || config.buildEs5);
setBooleanConfig(config, 'profile', 'profile', config.devMode);
setBooleanConfig(config, 'writeLog', 'log', false);
setBooleanConfig(config, 'buildAppCore', null, true);
Expand Down
1 change: 1 addition & 0 deletions src/declarations/stencil-public-compiler.ts
Expand Up @@ -568,6 +568,7 @@ export interface ConfigFlags {
e2e?: boolean;
emulate?: string;
es5?: boolean;
esm?: boolean;
headless?: boolean;
help?: boolean;
log?: boolean;
Expand Down

0 comments on commit a847a6e

Please sign in to comment.