@@ -768,3 +768,273 @@ let output_result = transform(TransformOptions {
768
768
specifier_mappings: None,
769
769
}).await?;
770
770
` ` `
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\n node_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