Cover image for Design System and its code source migration: a brief logbook - blogpost
Juliana Barros Lima (Jules)

Juliana Barros Lima (Jules)

06 Oct 2023 7 min read

Design System and its code source migration: a brief logbook

Sinopsis

The present blog post is a brief experience report from a source code migration from Javascript to Typescript. Therefore, we understand that some points may be more useful for your project as it becomes more like your project.

The Confetti itself is an open-source product developed by Labcodes, and it has a test suite and its repository can be found at: https://github.com/labcodes/confetti-ds. And its documentation is accessible at: https://confetti.labcodes.com.br/.

Another important point is that this migration was already planned and some events have helped to advance this process and as good practices have helped to guide this development process.

Context

While we were publishing the 0.1.0-alpha.11 version, the team has started to test the package into the RunKit (the Npm's integrated terminal), and we noticed that a project dependency @babel/runtime was not found. Indeed, it was known by the time that Babel (ES6 transpiler to Common JS) was causing some memory leakages during the build process.

One of the probable causes of these problems was the direct call to the Babel CLI plus the packaging and dependency mishaps; the direct consequence was the larger size of the final bundle.

After attempting to remove the package and review the build process, the team noticed that it was the ideal timing to update the build process using tsup to prepare the ground when the code source migrated to Typescript.

As each component has its own file, it allowed the team to better control the functioning of each one and gradually test each change. The high test coverage contributed positively to the migration, since the project had good coverage and allowed each refactoring step to be verified.

How was the process?

Adjusting the build

We used tsup as a build tool with TS, which in turn uses ES Build under the hood. However, it allows the user to custom and control the steps and parameters of code formatting. The setup itself was written inside a new file named tsup.config.ts, where we could select and filter the correct files for the build process (tsup can bundle any project with Node in its stack).

tsup.config.ts
tsup.config.ts

The result is a faster build process, as they describe in their official docs:

tsup automatically excludes packages specified in the dependencies and peerDependencies fields in the packages.json, but if it somehow doesn't exclude some packages, this library also has a special executable tsup-node that automatically skips bundling any Node.js package.

The insight

When we were testing the new build process, we noticed that the product has running smoothly and there were no major breaking changes, therefore the design system was stable.

After that overall analysis and the team's availability to perform a full source code migration plus the reasonably low cost of time, we certified that it was the perfect timing to start the process of migrating the components from JS to TS.

Getting down to the real deal

First, we started altering the file extensions from .js to .tsx. The Jest's VS Code extension started flagging errors in the test files - which was somehwat expected - and we also began to replace the file extensions to .tsx. Some error messages originated from a more tsup strict nature.

We therefore resumed the migration so that we could move forward with the refactorings without so many warnings. To do this, we removed ESLint (because tsup was already performing this task) and at the same time we disabled tsconfig's strict mode.

We also needed to export the component types via a type declaration file types.d.ts. During compilation, the types are extracted and exported, making them available to the API of the developer who wants to use the design system library.

Each component had its interface created: in the case of the base components, we created the base interface to be used in the child components - we'll come back to this part later because we had to make some changes at this point too. During this process of declaring and structuring the variables, the references to PropTypes were also removed.

Points of interest (& some tips!)

Throughout the process we noticed that some tests and work tools helped us to locate some issues along the way. It is important to emphasize the developer's acquaintance level with the project might interfere more than seniority, because those tools and test files can help and engineer to model the props.

Here are some tips from the migration process's notes:

1. Pay close attention to the optional props

Several interfaces had optional props that were marked as mandatory by accident when we migrated from PropTypes to types, and this caused several errors. Taking this into account is essential to validate the migration.

2. The preferred usage of interface over type

We prefer to use interfaces rather than types because they allow the user to customize components more safely, are easier to extend and are more rigorous in checking types.

3. Pay close attention when declaring types in components

As components are functions, the difference between the type of the arguments and the return type is a simple :. At the beginning of the process, it was common to put the types of the props as the return of the components.

4. Focus on the tests and spend some time setting up those to run automatically

As we started using Turborepo in the project, the VSCode Jest extension didn't run the tests because it couldn't find the correct project path.

5. Using the correct extensions for your IDE can help you save much time

For VS Code, these were the ones that had helped us most:

Error Lens is a native Microsoft's extension that helps to check the syntax errors. Total Typescript was developed by Matt Pollock - he has some great courses about Typescript (the link is avaliable at References section). Another native extension that had helped us a lot was Jest. It provied us a automated JS's test suite with a complete interface that had allowed us to perform a double-check to validate if the props were being rendered and being used as expected.

6. Take a closer look at the tests and docs

It is worth analyzing if there are still mentions of JS Docs declarations (if you use them), checking if there are components without mentions of props in the documentation; in addition, analyze if `type-checking` is impacting the tests.

7. Export all the components to a single index file

Having just one output file helps when generating an index.d.ts file and when importing the components into the project that will use the library. This allows another user to extend the types more easily.

8. For a component with multiple variants, create the base types and extend them; however, but only export the types for each variant

This topic is quite self-describing, as we can see in the following snippet: the base properties are not declared inside the abstract component (aka the base component); these are declared inside at a more generic interface.

index.tsx file for the Button component
index.tsx file for the Button component

types.d.ts file for the Button component
types.d.ts file for the Button component

Results

After the migration, the package's build time hasn't changed much, but there is now an additional step in the process, with the export of the types. The size of the exported source code has been reduced by around 28% compared to the build still in Javascript and Babel/ESLint.

However, the size of the package and the code itself have not canged significantly.

Before migration:

npm notice package size: 64.9 kB

npm notice unpacked size: 341.7 kB

After migration:

npm notice package size: 78.5 kB (31kb from types, 47kb of code)

npm notice unpacked size: 365.5 kB

Lessons Learned

The migration itself, despite its mishaps, was greatly helped by the wide coverage of the tests; therefore, code following good TDD practices can help to better understand certain stages of the change process.

In addition, the initial process was carried out with strict: False, i.e. we needed to set all static type validations to true. There are still parts of the code with an implicitly inferred `any` type, for example.

1. Always start tests from the inside out

Even if the tests have some implementation problems, they are the source of truth for your application and will immediately show up any serious problems during migration.

2. Have tests in your project, please 🙏

Projects with high test coverage are essential in DX to guarantee improvements and evolutions in the code, especially when it comes to such a large refactoring.

3. If in doubt, try to migrate to Typescript ASAP

The migration itself wasn't that complicated, but the effort would have increased greatly if the library had been larger, so this is the kind of decision that should be made as early as possible.

4. Remember to test in a real scenario (our case was an internal project)

The projects that already used the Confetti's design system did not need a lot of refactoring to update the code to receive the project updates.

Acknowledgements

I would like to thank my mentor Luciano Ratamero and one of the creators of the Confetti who agreed to help and without whom this migration would not have been possible and who co-authored this text.

References

https://www.typescriptlang.org/docs/handbook/declaration-files/templates/module-d-ts.html

https://github.com/labcodes/confetti-ds

https://www.youtube.com/watch?v=eh89VE3Mk5g

https://www.udemy.com/course/typescript-for-professionals/