Skip to content

Commit 9354cf0

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

1 file changed

Lines changed: 272 additions & 0 deletions

File tree

README.md

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,3 +768,275 @@ 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 them.
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+
// you should open it in actual
858+
test: false,
859+
importMap: importMap,
860+
package: packageJson,
861+
// custom by yourself
862+
compilerOptions: {
863+
lib: ["DOM", "ES2022"],
864+
target: "ES2022",
865+
emitDecoratorMetadata: true,
866+
},
867+
postBuild() {
868+
// steps to run after building and before running the tests
869+
Deno.copyFileSync(rootResolve("./LICENSE"), npmResolve("./LICENSE"));
870+
Deno.copyFileSync(
871+
packageResolve("./README.md"),
872+
npmResolve("./README.md"),
873+
);
874+
},
875+
...options,
876+
});
877+
};
878+
```
879+
880+
### Main Steps
881+
882+
1. Create two subfolders and add some project files
883+
884+
```shell
885+
# start from root
886+
mkdir packages/module-a
887+
cd packages/module-a
888+
echo "export const a = 1;" > index.ts
889+
echo "# @dnt-mono/module-a" > README.md
890+
npm init --scope @dnt-mono --yes # name: @dnt-mono/module-a
891+
```
892+
893+
Repeat the steps to create a `module-b` folder
894+
895+
```shell
896+
# start from root
897+
mkdir packages/module-b
898+
cd packages/module-b
899+
echo "import { a } from \"@dnt-mono/module-a\";\nexport const b = a + 1;" > index.ts
900+
echo "# @dnt-mono/module-b" > README.md
901+
npm init --scope @dnt-mono --yes # name: @dnt-mono/module-b
902+
903+
pnpm add @dnt-mono/module-a --workspace # add module-a as a dependency
904+
```
905+
906+
2. In this example, `module-b` depends on `module-a`, and we used the specifier
907+
`@dnt-mono/module-a` in the code, so we need some configurations to make the
908+
deno language server work correctly. In the `imports` field of `deno.json`,
909+
add these configurations:
910+
911+
```jsonc
912+
"@dnt-mono/module-a": "./packages/module-a/index.ts", // in imports
913+
"@dnt-mono/module-b": "./packages/module-b/index.ts" // in imports
914+
```
915+
916+
3. Next, create the build script and configuration files
917+
918+
1. `scripts/build_npm.ts`
919+
920+
```ts
921+
import { npmBuilder } from "./npmBuilder.ts";
922+
923+
const version = Deno.args[0];
924+
await npmBuilder({
925+
packageDir: import.meta.resolve("../packages/module-a/"),
926+
importMap: import.meta.resolve("./import_map.npm.json"),
927+
version,
928+
});
929+
await npmBuilder({
930+
packageDir: import.meta.resolve("../packages/module-b/"),
931+
importMap: import.meta.resolve("./import_map.npm.json"),
932+
version,
933+
});
934+
```
935+
936+
2. `scripts/import_map.npm.json`
937+
938+
```json
939+
{
940+
"imports": {
941+
"@dnt-mono/module-a": "npm:@dnt-mono/module-a",
942+
"@dnt-mono/module-b": "npm:@dnt-mono/module-b"
943+
}
944+
}
945+
```
946+
947+
4. Then, in your `deno.json`, configure the build command:
948+
949+
```jsonc
950+
"build": "deno run -A ./scripts/build_npm.ts" // in tasks
951+
```
952+
953+
5. Finally, try executing the build command to create the npm directory
954+
```shell
955+
deno task build
956+
```
957+
Now, you should see the npm directory has been populated with the module-a
958+
and module-b folders ready for npm publishing. You can try to publish these
959+
npm packages:
960+
```shell
961+
pnpm publish -r --no-git-checks --dry-run # you should remove --dry-run for an actual run
962+
```
963+
964+
### How It Works
965+
966+
1. We use deno as the language server, which is quite powerful, vastly improved
967+
from tsc itself through customized development.
968+
2. So here, the package.json is just a "template file" and not a configuration
969+
file. The only configuration file that goes into effect during development is
970+
deno.json.
971+
3. Hence, pnpm is just a tool for the final output built by dnt, meaning it only
972+
serves the `npm/*` directory. This is also why `pnpm-workspaces.yaml` is
973+
configured as it is.
974+
4. The `import_map.npm.json` used in dnt is essential. We can't use `deno.json`
975+
directly as `importMap` because `deno.json` is configured for the deno
976+
language server, while `import_map.npm.json` is for dnt/pnpm use. In complex
977+
projects, it's advisable to manage it automatically with a script.
978+
979+
### Advanced Tips
980+
981+
In deno development, our philosophy is file-oriented rather than
982+
module-oriented. Therefore, if needed, you may want to add this kind of
983+
configuration in `deno.json`:
984+
985+
```jsonc
986+
{
987+
// ...
988+
"imports": {
989+
// ...
990+
"@dnt-mono/module-a": "./packages/module-a/index.ts",
991+
"@dnt-mono/module-a/": "./packages/module-a/src/",
992+
"@dnt-mono/module-b": "./packages/module-b/index.ts",
993+
"@dnt-mono/module-b/": "./packages/module-b/src/"
994+
// ...
995+
}
996+
}
997+
```
998+
999+
I prefer to put files other than `index.ts` into a `src` directory, which aligns
1000+
more with the style of node projects.
1001+
1002+
> However, remember not to move the `index.ts` file to the `src` directory as
1003+
> well, as it could cause exceptions
1004+
> [#249](https://github.com/denoland/dnt/issues/249).
1005+
1006+
Then, it's about the dnt configuration, where you need to iterate over all your
1007+
files and configure them in the entryPoints:
1008+
1009+
```ts
1010+
build({
1011+
entryPoints: [
1012+
// default entry
1013+
{ name: ".", path: packageResolve("./index.ts") },
1014+
// src files
1015+
ALL_SRC_TS_FILES.map((name) => ({
1016+
name: `./${name}`,
1017+
path: `./src/${name}`,
1018+
})),
1019+
],
1020+
// ...
1021+
});
1022+
```
1023+
1024+
Now, you can write code like this:
1025+
1026+
```ts
1027+
import { xxx } from "@dnt-mono/module-a/xxx.ts";
1028+
```
1029+
1030+
### Points to Note
1031+
1032+
1. Plan your project structure well to avoid cyclic dependencies. If needed, you
1033+
should configure peerDependencies yourself.
1034+
2. Don't self-import within a module.
1035+
> The language server doesn't understand that you intend to publish to npm,
1036+
> so even if deno works correctly, your goal is to make it work with node as
1037+
> well.
1038+
```ts
1039+
import { a } from "@dnt-mono/module-a"; // don't import module-a in module-a
1040+
```
1041+
It is advisable to write lint rules to avoid these mistakes in actual
1042+
projects.

0 commit comments

Comments
 (0)