Skip to content

Commit 53c5984

Browse files
committed
:docs add docs about dnt 💗 monorepo
Add documentation describing how to compile Monorepo-style projects with dnt.
1 parent 07ccc72 commit 53c5984

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed

README.md

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,3 +768,273 @@ let output_result = transform(TransformOptions {
768768
specifier_mappings: None,
769769
}).await?;
770770
```
771+
772+
## Publishing Your Deno Project as a Monorepo using dnt
773+
774+
> Before providing theoretical guidance, let's look at how to achieve this in
775+
> practice. After completion, I will explain the advantages of this project
776+
> management solution.
777+
778+
### Tools
779+
780+
1. [deno](https://deno.com/)
781+
2. [pnpm](https://pnpm.io/installation)
782+
783+
### Preparation
784+
785+
1. Create your project:
786+
```shell
787+
deno init dnt-mono
788+
# cd dnt-mono
789+
# code . # open in ide
790+
```
791+
2. Initialize a git repository
792+
```shell
793+
git init
794+
echo "npm\nnode_modules" > .gitignore # ignore the npm folder
795+
```
796+
3. Initialize package.json, and other files typically required by npm/pnpm
797+
```shell
798+
npm init --yes --private # create a package.json file
799+
echo "MIT" > LICENSE
800+
echo "# Hello Dnt ❤️ Monorepo" > README.md
801+
echo "packages:\n - \"npm/*\"" > pnpm-workspace.yaml
802+
```
803+
4. Prepare the dnt script
804+
805+
```shell
806+
deno add @deno/dnt
807+
```
808+
809+
Refer to [Setup](https://github.com/denoland/dnt?tab=readme-ov-file#setup),
810+
as we need to build multiple npm packages, create the `scripts/npmBuilder.ts`
811+
file:
812+
813+
```ts
814+
import { build, BuildOptions, emptyDir } from "@deno/dnt";
815+
import fs from "node:fs";
816+
import { fileURLToPath, pathToFileURL } from "node:url";
817+
818+
const rootDir = import.meta.resolve("../");
819+
const rootResolve = (path: string) => fileURLToPath(new URL(path, rootDir));
820+
export const npmBuilder = async (config: {
821+
packageDir: string;
822+
version?: string;
823+
importMap?: string;
824+
options?: Partial<BuildOptions>;
825+
}) => {
826+
const { packageDir, version, importMap, options } = config;
827+
const packageResolve = (path: string) =>
828+
fileURLToPath(new URL(path, packageDir));
829+
const packageJson = JSON.parse(
830+
fs.readFileSync(packageResolve("./package.json"), "utf-8"),
831+
);
832+
// remove some field which dnt will create. if you known how dnt work, you can keep theme.
833+
delete packageJson.main;
834+
delete packageJson.module;
835+
delete packageJson.exports;
836+
837+
console.log(`\nstart dnt: ${packageJson.name}`);
838+
839+
const npmDir = pathToFileURL(
840+
rootResolve(`./npm/${packageJson.name.split("/").pop()}`),
841+
).href;
842+
const npmResolve = (path: string) => fileURLToPath(new URL(path, npmDir));
843+
844+
await emptyDir(npmDir);
845+
846+
if (version) {
847+
Object.assign(packageJson, { version: version });
848+
}
849+
850+
await build({
851+
entryPoints: [{ name: ".", path: packageResolve("./index.ts") }],
852+
outDir: npmDir,
853+
packageManager: "pnpm",
854+
shims: {
855+
deno: true,
856+
},
857+
importMap: importMap,
858+
package: packageJson,
859+
// custom by yourself
860+
compilerOptions: {
861+
lib: ["DOM", "ES2022"],
862+
target: "ES2022",
863+
emitDecoratorMetadata: true,
864+
},
865+
postBuild() {
866+
// steps to run after building and before running the tests
867+
Deno.copyFileSync(rootResolve("./LICENSE"), npmResolve("./LICENSE"));
868+
Deno.copyFileSync(
869+
packageResolve("./README.md"),
870+
npmResolve("./README.md"),
871+
);
872+
},
873+
...options,
874+
});
875+
};
876+
```
877+
878+
### Main Steps
879+
880+
1. Create two subfolders and add some project files
881+
882+
```shell
883+
# start from root
884+
mkdir packages/module-a
885+
cd packages/module-a
886+
echo "export const a = 1;" > index.ts
887+
echo "# @dnt-mono/module-a" > README.md
888+
npm init --scope @dnt-mono --yes # name: @dnt-mono/module-a
889+
```
890+
891+
Repeat the steps to create a `module-b` folder
892+
893+
```shell
894+
# start from root
895+
mkdir packages/module-b
896+
cd packages/module-b
897+
echo "import { a } from \"@dnt-mono/module-a\";\nexport const b = a + 1;" > index.ts
898+
echo "# @dnt-mono/module-b" > README.md
899+
npm init --scope @dnt-mono --yes # name: @dnt-mono/module-b
900+
901+
pnpm add @dnt-mono/module-a --workspace # add module-a as a dependency
902+
```
903+
904+
2. In this example, `module-b` depends on `module-a`, and we used the specifier
905+
`@dnt-mono/module-a` in the code, so we need some configurations to make the
906+
deno language server work correctly. In the `imports` field of `deno.json`,
907+
add these configurations:
908+
909+
```jsonc
910+
"@dnt-mono/module-a": "./packages/module-a/index.ts", // in imports
911+
"@dnt-mono/module-b": "./packages/module-b/index.ts" // in imports
912+
```
913+
914+
3. Next, create the build script and configuration files
915+
916+
1. `scripts/build_npm.ts`
917+
918+
```ts
919+
import { npmBuilder } from "./npmBuilder.ts";
920+
921+
const version = Deno.args[0];
922+
await npmBuilder({
923+
packageDir: import.meta.resolve("../packages/module-a/"),
924+
importMap: import.meta.resolve("./import_map.npm.json"),
925+
version,
926+
});
927+
await npmBuilder({
928+
packageDir: import.meta.resolve("../packages/module-b/"),
929+
importMap: import.meta.resolve("./import_map.npm.json"),
930+
version,
931+
});
932+
```
933+
934+
2. `scripts/import_map.npm.json`
935+
936+
```json
937+
{
938+
"imports": {
939+
"@dnt-mono/module-a": "npm:@dnt-mono/module-a",
940+
"@dnt-mono/module-b": "npm:@dnt-mono/module-b"
941+
}
942+
}
943+
```
944+
945+
4. Then, in your `deno.json`, configure the build command:
946+
947+
```jsonc
948+
"build": "deno run -A ./scripts/build_npm.ts" // in tasks
949+
```
950+
951+
5. Finally, try executing the build command to create the npm directory
952+
```shell
953+
deno task build
954+
```
955+
Now, you should see the npm directory has been populated with the module-a
956+
and module-b folders ready for npm publishing. You can try to publish these
957+
npm packages:
958+
```shell
959+
pnpm publish -r --no-git-checks --dry-run # you should remove --dry-run for an actual run
960+
```
961+
962+
### How It Works
963+
964+
1. We use deno as the language server, which is quite powerful, vastly improved
965+
from tsc itself through customized development.
966+
2. So here, the package.json is just a "template file" and not a configuration
967+
file. The only configuration file that goes into effect during development is
968+
deno.json.
969+
3. Hence, pnpm is just a tool for the final output built by dnt, meaning it only
970+
serves the `npm/*` directory. This is also why `pnpm-workspaces.yaml` is
971+
configured as it is.
972+
4. The `import_map.npm.json` used in dnt is essential. We can't use `deno.json`
973+
directly as `importMap` because `deno.json` is configured for the deno
974+
language server, while `import_map.npm.json` is for dnt/pnpm use. In complex
975+
projects, it's advisable to manage it automatically with a script.
976+
977+
### Advanced Tips
978+
979+
In deno development, our philosophy is file-oriented rather than
980+
module-oriented. Therefore, if needed, you may want to add this kind of
981+
configuration in `deno.json`:
982+
983+
```jsonc
984+
{
985+
// ...
986+
"imports": {
987+
// ...
988+
"@dnt-mono/module-a": "./packages/module-a/index.ts",
989+
"@dnt-mono/module-a/": "./packages/module-a/src/",
990+
"@dnt-mono/module-b": "./packages/module-b/index.ts",
991+
"@dnt-mono/module-b/": "./packages/module-b/src/"
992+
// ...
993+
}
994+
}
995+
```
996+
997+
I prefer to put files other than `index.ts` into a `src` directory, which aligns
998+
more with the style of node projects.
999+
1000+
> However, remember not to move the `index.ts` file to the `src` directory as
1001+
> well, as it could cause exceptions
1002+
> [#249](https://github.com/denoland/dnt/issues/249).
1003+
1004+
Then, it's about the dnt configuration, where you need to iterate over all your
1005+
files and configure them in the entryPoints:
1006+
1007+
```ts
1008+
build({
1009+
entryPoints: [
1010+
// default entry
1011+
{ name: ".", path: packageResolve("./index.ts") },
1012+
// src files
1013+
ALL_SRC_TS_FILES.map((name) => ({
1014+
name: `./${name}`,
1015+
path: `./src/${name}`,
1016+
})),
1017+
],
1018+
// ...
1019+
});
1020+
```
1021+
1022+
Now, you can write code like this:
1023+
1024+
```ts
1025+
import { xxx } from "@dnt-mono/module-a/xxx.ts";
1026+
```
1027+
1028+
### Points to Note
1029+
1030+
1. Plan your project structure well to avoid cyclic dependencies. If needed, you
1031+
should configure peerDependencies yourself.
1032+
2. Don't self-import within a module.
1033+
> The language server doesn't understand that you intend to publish to npm,
1034+
> so even if deno works correctly, your goal is to make it work with node as
1035+
> well.
1036+
```ts
1037+
import { a } from "@dnt-mono/module-a"; // don't import module-a in module-a
1038+
```
1039+
It is advisable to write lint rules to avoid these mistakes in actual
1040+
projects.

0 commit comments

Comments
 (0)