├── .gitignore ├── version.ts ├── vendor └── https │ └── deno.land │ └── std │ ├── path │ └── mod.ts │ ├── fmt │ └── printf.ts │ └── http │ └── server.ts ├── example └── simple │ ├── vendor │ └── https │ │ └── deno.land │ │ └── std │ │ └── http │ │ └── server.ts │ ├── README.md │ ├── dem.json │ └── server.ts ├── net.ts ├── tsconfig.json ├── dem.json ├── store.ts ├── egg.json ├── .github └── workflows │ ├── test.yml │ └── publish-to-nest.land.yml ├── config.ts ├── LICENSE.md ├── module.ts ├── Makefile ├── actions.ts ├── ast.ts ├── cmd.ts ├── README.md ├── repository.ts ├── mod.ts └── mutations.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .envrc 3 | .vscode -------------------------------------------------------------------------------- /version.ts: -------------------------------------------------------------------------------- 1 | export const version = "0.9.9"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/path/mod.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.105.0/path/mod.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/fmt/printf.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.105.0/fmt/printf.ts"; 2 | -------------------------------------------------------------------------------- /vendor/https/deno.land/std/http/server.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@0.105.0/http/server.ts"; 2 | -------------------------------------------------------------------------------- /example/simple/vendor/https/deno.land/std/http/server.ts: -------------------------------------------------------------------------------- 1 | export * from "https://deno.land/std@v0.51.0/http/server.ts"; 2 | -------------------------------------------------------------------------------- /example/simple/README.md: -------------------------------------------------------------------------------- 1 | # simple HTTP server example 2 | 3 | ## Usage 4 | 5 | ``` 6 | deno --allow-net server.ts 7 | ``` 8 | -------------------------------------------------------------------------------- /net.ts: -------------------------------------------------------------------------------- 1 | export function createURL( 2 | moduleProtocol: string, 3 | modulePath: string, 4 | moduleVersion: string, 5 | filePath: string, 6 | ): string { 7 | return `${moduleProtocol}://${modulePath}@${moduleVersion}${filePath}`; 8 | } 9 | -------------------------------------------------------------------------------- /example/simple/dem.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | { 4 | "protocol": "https", 5 | "path": "deno.land/std", 6 | "version": "v0.51.0", 7 | "files": [ 8 | "/http/server.ts" 9 | ] 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noImplicitAny": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "noImplicitReturns": true, 8 | "noFallthroughCasesInSwitch": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /dem.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules": [ 3 | { 4 | "protocol": "https", 5 | "path": "deno.land/std", 6 | "version": "0.105.0", 7 | "files": [ 8 | "/fmt/printf.ts", 9 | "/http/server.ts", 10 | "/path/mod.ts" 11 | ] 12 | } 13 | ], 14 | "aliases": {} 15 | } 16 | -------------------------------------------------------------------------------- /store.ts: -------------------------------------------------------------------------------- 1 | import { Config } from "./config.ts"; 2 | 3 | export type Store = { 4 | config: Config; 5 | }; 6 | 7 | export function duplicateStore(s: Store): Store { 8 | return { 9 | config: { 10 | modules: [...s.config.modules], 11 | aliases: { 12 | ...s.config.aliases, 13 | }, 14 | }, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /example/simple/server.ts: -------------------------------------------------------------------------------- 1 | // import dependency from vendor. 2 | import { serve } from "./vendor/https/deno.land/std/http/server.ts"; 3 | 4 | const body = new TextEncoder().encode("Hello World\n"); 5 | const s = serve(":8000"); 6 | window.onload = async () => { 7 | console.log("http://localhost:8000/"); 8 | for await (const req of s) { 9 | req.respond({ body }); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dem", 3 | "description": "A module version manager for Deno.", 4 | "homepage": "https://github.com/syumai/dem", 5 | "files": [ 6 | "./**/*.ts", 7 | "README.md" 8 | ], 9 | "entry": "./mod.ts" 10 | } -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | schedule: 9 | - cron: '0 15 * * *' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup Deno environment 17 | uses: denolib/setup-deno@master 18 | with: 19 | deno-version: "v1.x" 20 | - name: Show Deno version 21 | run: deno --version 22 | - name: Run lint 23 | run: make lint 24 | - name: Install dem-local 25 | run: | 26 | make install-local 27 | echo $HOME/.deno/bin >> $GITHUB_PATH 28 | - name: Run tests 29 | run: | 30 | make test 31 | -------------------------------------------------------------------------------- /.github/workflows/publish-to-nest.land.yml: -------------------------------------------------------------------------------- 1 | name: "publish current release to https://nest.land" 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | publishToNestDotLand: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Setup repo 14 | uses: actions/checkout@v2 15 | 16 | - name: "setup" # check: https://github.com/actions/virtual-environments/issues/1777 17 | uses: denolib/setup-deno@v2 18 | with: 19 | deno-version: v1.4.6 20 | 21 | - name: "check nest.land" 22 | run: | 23 | deno run --allow-net --allow-read --allow-run https://deno.land/x/cicd/publish-on-nest.land.ts ${{ secrets.GITHUB_TOKEN }} ${{ secrets.NESTAPIKEY }} ${{ github.repository }} -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "./module.ts"; 2 | 3 | export type Config = { 4 | modules: Module[]; 5 | aliases: { 6 | [name: string]: string; 7 | }; 8 | }; 9 | 10 | export function validateConfig(config: Config) { 11 | if (typeof config !== "object") { 12 | throw new Error(`config type must be 'object'. actual: '${typeof config}'`); 13 | } 14 | if (!Array.isArray(config.modules)) { 15 | throw new Error( 16 | `version type must be Array. actual: '${typeof config.modules}'`, 17 | ); 18 | } 19 | config.modules.forEach((mod, i) => { 20 | if (!mod.protocol || !mod.path || !mod.files) { 21 | throw new Error( 22 | `module format is invalid. index: ${i}, protocol: ${mod.protocol}, path: ${mod.path}`, 23 | ); 24 | } 25 | }); 26 | if (typeof config.aliases !== "object") { 27 | throw new Error( 28 | `config.aliases type must be 'object'. actual: '${typeof config 29 | .aliases}'`, 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019-present [syumai](https://github.com/syumai/) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /module.ts: -------------------------------------------------------------------------------- 1 | const moduleRegex = /([^@]+)@([^/]+)\/?/; 2 | 3 | export class Module { 4 | constructor( 5 | public readonly protocol: string, 6 | public readonly path: string, 7 | public version: string, 8 | public files: string[], 9 | ) {} 10 | 11 | toString(): string { 12 | return `${this.protocol}://${this.path}`; 13 | } 14 | 15 | toStringWithVersion(): string { 16 | return `${this.toString()}@${this.version}`; 17 | } 18 | 19 | static parse = (name: string): Module => { 20 | const u = new URL(name); 21 | const { protocol, hostname, pathname } = u; 22 | const result = pathname.match(moduleRegex); 23 | let path = ""; 24 | let version = ""; 25 | if (result) { 26 | path = hostname + result[1]; 27 | version = result[2]; 28 | } else { 29 | path = hostname + pathname; 30 | } 31 | return new Module(protocol.replace(/\:$/, ""), path, version, []); 32 | }; 33 | } 34 | 35 | export function compareModules(modA: Module, modB: Module): number { 36 | return modA.path.localeCompare(modB.path); 37 | } 38 | 39 | export function moduleEquals(a: Module, b: Module): boolean { 40 | return a.protocol === b.protocol && a.path === b.path; 41 | } 42 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL=/bin/bash 2 | TARGET_SRC=$(shell shopt -s globstar && ls ./*.ts | grep -v ./vendor) 3 | 4 | lint: 5 | deno fmt --check $(TARGET_SRC) 6 | deno lint --unstable $(TARGET_SRC) 7 | 8 | fmt: 9 | deno fmt $(TARGET_SRC) 10 | 11 | install-local: 12 | deno install --allow-read --allow-write --allow-net -f -n dem-local ./cmd.ts 13 | 14 | test: test/cmd 15 | 16 | test/cmd: 17 | # test ensure / prune 18 | mkdir -p tmp/welcome 19 | echo "import './vendor/welcome.ts'" > tmp/welcome/mod.ts 20 | cd tmp/welcome && \ 21 | dem-local init && \ 22 | dem-local add https://deno.land/std@0.73.0 && \ 23 | dem-local alias https://deno.land/std/examples/welcome.ts welcome.ts && \ 24 | dem-local ensure && \ 25 | dem-local prune 26 | deno run -r -c ./tsconfig.json tmp/welcome/mod.ts | grep -q 'Welcome to Deno' 27 | 28 | # test unlink / link 29 | rm tmp/welcome/mod.ts 30 | echo "import './vendor/https/deno.land/std/examples/welcome.ts'" > tmp/welcome/mod.ts 31 | cd tmp/welcome && \ 32 | dem-local unalias welcome.ts && \ 33 | dem-local unlink https://deno.land/std/examples/welcome.ts && \ 34 | dem-local remove https://deno.land/std && \ 35 | dem-local add https://deno.land/std@0.73.0 && \ 36 | dem-local link https://deno.land/std/examples/welcome.ts 37 | deno run -r -c ./tsconfig.json tmp/welcome/mod.ts | grep -q 'Welcome to Deno' 38 | 39 | rm -rf tmp/welcome 40 | 41 | .PHONY: lint fmt install-local test test/cmd 42 | -------------------------------------------------------------------------------- /actions.ts: -------------------------------------------------------------------------------- 1 | import { Module } from "./module.ts"; 2 | export const ADD_MODULE = "ADD_MODULE" as const; 3 | export const REMOVE_MODULE = "REMOVE_MODULE" as const; 4 | export const ADD_LINK = "ADD_LINK" as const; 5 | export const REMOVE_LINK = "REMOVE_LINK" as const; 6 | export const ADD_ALIAS = "ADD_ALIAS" as const; 7 | export const REMOVE_ALIAS = "REMOVE_ALIAS" as const; 8 | export const UPDATE_MODULE = "UPDATE_MODULE" as const; 9 | 10 | export interface AddModuleAction { 11 | type: typeof ADD_MODULE; 12 | payload: { 13 | module: Module; 14 | }; 15 | } 16 | 17 | export interface RemoveModuleAction { 18 | type: typeof REMOVE_MODULE; 19 | payload: { 20 | moduleProtocol: string; 21 | modulePath: string; 22 | }; 23 | } 24 | 25 | export interface AddLinkAction { 26 | type: typeof ADD_LINK; 27 | payload: { 28 | link: string; 29 | }; 30 | } 31 | 32 | export interface RemoveLinkAction { 33 | type: typeof REMOVE_LINK; 34 | payload: { 35 | link: string; 36 | }; 37 | } 38 | 39 | export interface AddAliasAction { 40 | type: typeof ADD_ALIAS; 41 | payload: { 42 | aliasTargetPath: string; 43 | aliasPath: string; 44 | }; 45 | } 46 | 47 | export interface RemoveAliasAction { 48 | type: typeof REMOVE_ALIAS; 49 | payload: { 50 | aliasPath: string; 51 | }; 52 | } 53 | 54 | export interface UpdateModuleAction { 55 | type: typeof UPDATE_MODULE; 56 | payload: { 57 | moduleProtocol: string; 58 | modulePath: string; 59 | moduleVersion: string; 60 | }; 61 | } 62 | 63 | export type Action = 64 | | AddModuleAction 65 | | RemoveModuleAction 66 | | AddLinkAction 67 | | RemoveLinkAction 68 | | AddAliasAction 69 | | RemoveAliasAction 70 | | UpdateModuleAction; 71 | -------------------------------------------------------------------------------- /ast.ts: -------------------------------------------------------------------------------- 1 | import ts from "https://esm.sh/typescript@4.4.2"; 2 | import * as path from "./vendor/https/deno.land/std/path/mod.ts"; 3 | 4 | const dec = new TextDecoder("utf-8"); 5 | 6 | export async function hasDefaultExportLocal( 7 | filePath: string, 8 | ): Promise { 9 | const body = dec.decode(await Deno.readFile(filePath)); 10 | return hasDefaultExport(body); 11 | } 12 | 13 | export async function hasDefaultExportRemote(url: string): Promise { 14 | const res = await fetch(url, { redirect: "follow" }); 15 | const body = await res.text(); 16 | return hasDefaultExport(body); 17 | } 18 | 19 | export function hasDefaultExport(body: string): boolean { 20 | const sourceFile = ts.createSourceFile("", body, ts.ScriptTarget.ES2020); 21 | let hasDefault = false; 22 | sourceFile.forEachChild((node: ts.Node) => { 23 | hasDefault = hasDefault || node.kind === ts.SyntaxKind.ExportAssignment; 24 | }); 25 | return hasDefault; 26 | } 27 | 28 | function removeQuotes(s: string): string { 29 | return s.replace(/[\'\"\`]/g, ""); 30 | } 31 | 32 | const crawlImport = (filePaths: string[], sourceFile: ts.SourceFile) => 33 | ( 34 | node: ts.Node, 35 | ) => { 36 | if (node.kind === ts.SyntaxKind.ImportDeclaration) { 37 | node.forEachChild((child: ts.Node) => { 38 | if (child.kind === ts.SyntaxKind.StringLiteral) { 39 | filePaths.push(removeQuotes(child.getText(sourceFile))); 40 | } 41 | }); 42 | } 43 | }; 44 | 45 | async function getImportFilePaths( 46 | dirName: string, 47 | excludes: string[], 48 | ): Promise { 49 | const filePaths: string[] = []; 50 | for await (const f of Deno.readDir(dirName)) { 51 | if (!f.name) { 52 | continue; 53 | } 54 | if (f.isFile && f.name.match(/\.(js|ts)x?$/)) { 55 | const body = await Deno.readFile(path.join(dirName, f.name)); 56 | const sourceFile = ts.createSourceFile( 57 | f.name, 58 | dec.decode(body), 59 | ts.ScriptTarget.ES2020, 60 | ); 61 | sourceFile.forEachChild(crawlImport(filePaths, sourceFile)); 62 | } else if (f.isDirectory && !excludes.includes(f.name)) { 63 | const result = await getImportFilePaths( 64 | path.join(dirName, f.name), 65 | excludes, 66 | ); 67 | filePaths.push(...result); 68 | } 69 | } 70 | return filePaths; 71 | } 72 | 73 | export async function getFormattedImportFilePaths( 74 | dirName: string, 75 | excludes: string[], 76 | ): Promise { 77 | return (await getImportFilePaths(dirName, excludes)) 78 | .filter((f) => f.match(/vendor/)) 79 | .map((f) => f.replace(/^.+vendor\//, "")) 80 | .map((f) => f.replace(/\//, "://")); 81 | } 82 | -------------------------------------------------------------------------------- /cmd.ts: -------------------------------------------------------------------------------- 1 | import { App } from "./mod.ts"; 2 | import { version } from "./version.ts"; 3 | import { StorageRepository } from "./repository.ts"; 4 | import { Store } from "./store.ts"; 5 | import { Config } from "./config.ts"; 6 | 7 | const defaultConfigFilePath = "dem.json"; 8 | 9 | enum SubCommandType { 10 | Version = "version", 11 | Init = "init", 12 | Add = "add", 13 | Link = "link", 14 | Update = "update", 15 | Remove = "remove", 16 | Unlink = "unlink", 17 | Ensure = "ensure", 18 | Prune = "prune", 19 | Alias = "alias", 20 | Unalias = "unalias", 21 | } 22 | 23 | function isSubCommandType(t: string): t is SubCommandType { 24 | const commandTypes = Object.values(SubCommandType) as string[]; 25 | return commandTypes.includes(t); 26 | } 27 | 28 | async function main(args: string[]): Promise { 29 | const subCmdType = args[0]; 30 | if (!subCmdType) { 31 | const subCmdTypes = Object.values(SubCommandType).join(", "); 32 | console.error(`sub command must be given: ${subCmdTypes}`); 33 | return; 34 | } 35 | if (!isSubCommandType(subCmdType)) { 36 | console.error(`sub command ${subCmdType} does not exist.`); 37 | return; 38 | } 39 | 40 | const repo = new StorageRepository(defaultConfigFilePath); 41 | 42 | let config: Config; 43 | try { 44 | config = await repo.loadConfig(); 45 | } catch (e) { 46 | if (!(e instanceof Deno.errors.NotFound)) { 47 | console.error(`failed to get config, ${e}`); 48 | return; 49 | } 50 | config = { 51 | modules: [], 52 | aliases: {}, 53 | }; 54 | } 55 | const store: Store = { config }; 56 | 57 | const dem = new App(store, repo); 58 | 59 | const excludes = ["vendor", "node_modules"]; 60 | switch (subCmdType) { 61 | case SubCommandType.Version: 62 | console.log(`dem: ${version}`); 63 | break; 64 | case SubCommandType.Init: 65 | dem.init(); 66 | break; 67 | case SubCommandType.Add: 68 | dem.addModule(args[1]); 69 | break; 70 | case SubCommandType.Link: 71 | dem.addLink(args[1]); 72 | break; 73 | case SubCommandType.Update: 74 | dem.updateModule(args[1]); 75 | break; 76 | case SubCommandType.Unlink: 77 | dem.removeLink(args[1]); 78 | break; 79 | case SubCommandType.Remove: 80 | dem.removeModule(args[1]); 81 | break; 82 | case SubCommandType.Ensure: 83 | dem.ensure(excludes); 84 | break; 85 | case SubCommandType.Prune: 86 | dem.prune(excludes); 87 | break; 88 | case SubCommandType.Alias: 89 | dem.addAlias(args[1], args[2]); 90 | break; 91 | case SubCommandType.Unalias: 92 | dem.removeAlias(args[1]); 93 | break; 94 | } 95 | } 96 | 97 | if (import.meta.main) { 98 | let { args } = Deno; 99 | if (args[0] === "--") { 100 | args = args.slice(1); 101 | } 102 | main(args); 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dem 2 | 3 | [![Build Status](https://github.com/syumai/dem/workflows/test/badge.svg?branch=master)](https://github.com/syumai/dem/actions) 4 | 5 | - A simple module version manager for Deno. 6 | - dem creates `dem.json` and links script files with version. 7 | - linked scripts are stored in `vendor` directory. 8 | - dem provides an **easy way to update dependencies.** 9 | 10 | ## Example 11 | 12 | **Using `https://deno.land/std/http/server.ts` at `v0.51.0`** 13 | 14 | - `dem.json` 15 | - generated file like a package.json 16 | 17 | ```json 18 | { 19 | "modules": [ 20 | { 21 | "protocol": "https", 22 | "path": "deno.land/std", 23 | "version": "v0.51.0", 24 | "files": [ 25 | "/http/server.ts" 26 | ] 27 | } 28 | ] 29 | } 30 | ``` 31 | 32 | - `vendor/https/deno.land/std/http/server.ts` 33 | - generated linked script includes version specified in `dem.json` 34 | 35 | ```ts 36 | export * from "https://deno.land/std@v0.51.0/http/server.ts"; 37 | ``` 38 | 39 | - `example.ts` 40 | - script to run 41 | 42 | ```ts 43 | // You can import module without the version. 44 | import { serve } from "./vendor/https/deno.land/std/http/server.ts"; 45 | ``` 46 | 47 | ## Install 48 | 49 | ```sh 50 | deno install --allow-read --allow-write --allow-net -f -n dem https://deno.land/x/dem@0.9.9/cmd.ts 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Getting Started 56 | 57 | #### 1. `dem init` to create `dem.json` 58 | 59 | ```sh 60 | $ dem init 61 | successfully initialized the project. 62 | ``` 63 | 64 | #### 2. `dem add` to add module. 65 | 66 | ```sh 67 | $ dem add https://deno.land/std@v0.51.0 68 | successfully added new module: https://deno.land/std, version: v0.51.0 69 | ``` 70 | 71 | #### 3. Edit your script and import module from `vendor`. 72 | 73 | `example.ts` 74 | 75 | ```ts 76 | import * as path from './vendor/https/deno.land/std/fs/path.ts'; 77 | 78 | console.log(path.join(Deno.cwd(), 'example')); 79 | ``` 80 | 81 | #### 4. `dem ensure` to resolve modules. 82 | 83 | ```sh 84 | $ dem ensure 85 | successfully created link: https://deno.land/std@v0.51.0/fs/path.ts 86 | ``` 87 | 88 | #### 5. Run your script. 89 | 90 | ```sh 91 | $ deno example.ts 92 | ``` 93 | 94 | ## Standard Usage 95 | 96 | ### Updating module 97 | 98 | * `dem update` updates versions in `dem.json` and linked scripts. 99 | 100 | ```sh 101 | $ dem update https://deno.land/std@v0.52.0 102 | successfully updated module: https://deno.land/std, version: v0.52.0 103 | ``` 104 | 105 | ## Advanced Usage 106 | 107 | ### Alias 108 | 109 | * With alias, shortened script paths are available. 110 | 111 | `example.ts` 112 | 113 | ```ts 114 | // Original 115 | import * as dejs from "./vendor/https/deno.land/x/dejs/mod.ts" 116 | // With alias 117 | import * as dejs from "./vendor/dejs.ts" 118 | ``` 119 | 120 | #### How to use alias 121 | 122 | 1. execute `dem alias`. 123 | 124 | ```sh 125 | # create alias for module. 126 | $ dem alias https://deno.land/x/dejs/mod.ts dejs.ts # ./vendor/dejs.ts will be created. 127 | ``` 128 | 129 | 2. import script using alias path 130 | 131 | ```ts 132 | import * as dejs from "./vendor/dejs.ts" 133 | ``` 134 | 135 | 3. run `dem ensure`. 136 | 137 | ```sh 138 | # create link for `./vendor/https/deno.land/x/dejs/mod.ts`. 139 | $ dem ensure 140 | ``` 141 | 142 | ## Commands 143 | 144 | ```sh 145 | dem init # initialize dem.json 146 | dem add https://deno.land/x/dejs@0.1.0 # add module `dejs` and set its version to `0.1.0` 147 | dem link https://deno.land/x/dejs/mod.ts # create link of `dejs@0.1.0/mod.ts` and put it into vendor. 148 | dem update https://deno.land/x/dejs@0.2.0 # update module to `0.2.0` 149 | dem unlink https://deno.land/x/dejs/mod.ts # remove link of `dejs@0.2.0/mod.ts`. 150 | dem remove https://deno.land/x/dejs # remove module `dejs` 151 | dem ensure # resolve file paths of added modules used in project and link them. 152 | dem prune # remove unused modules and linked scripts. 153 | dem alias https://deno.land/x/dejs/mod.ts dejs.ts # create alias for module and put it into vendor. 154 | dem unalias dejs.ts # remove alias for module. 155 | ``` 156 | 157 | ### Unsupported features 158 | 159 | - [x] default export 160 | - [ ] manage `.d.ts` file 161 | 162 | ## Author 163 | 164 | syumai 165 | 166 | ## License 167 | 168 | MIT 169 | -------------------------------------------------------------------------------- /repository.ts: -------------------------------------------------------------------------------- 1 | import { Config, validateConfig } from "./config.ts"; 2 | import { Module } from "./module.ts"; 3 | import * as path from "./vendor/https/deno.land/std/path/mod.ts"; 4 | import { sprintf } from "./vendor/https/deno.land/std/fmt/printf.ts"; 5 | import { createURL } from "./net.ts"; 6 | 7 | const vendorDirectoryPath = "vendor"; 8 | 9 | export type Repository = { 10 | removeModule(moduleProtocol: string, modulePath: string): Promise; 11 | addLink( 12 | moduleProtocol: string, 13 | modulePath: string, 14 | moduleVersion: string, 15 | filePath: string, 16 | hasDefaultExport: boolean, 17 | ): Promise; 18 | removeLink( 19 | moduleProtocol: string, 20 | modulePath: string, 21 | filePath: string, 22 | ): Promise; 23 | addAlias( 24 | moduleProtocol: string, 25 | modulePath: string, 26 | filePath: string, 27 | aliasPath: string, 28 | hasDefaultExport: boolean, 29 | ): Promise; 30 | removeAlias(aliasPath: string): Promise; 31 | updateLink( 32 | moduleProtocol: string, 33 | modulePath: string, 34 | moduleVersion: string, 35 | filePath: string, 36 | hasDefaultExport: boolean, 37 | ): Promise; 38 | loadConfig(): Promise; 39 | saveConfig(config: Config): void; 40 | }; 41 | 42 | const dec = new TextDecoder("utf-8"); 43 | const enc = new TextEncoder(); 44 | 45 | export class StorageRepository { 46 | constructor(private filePath: string) {} 47 | 48 | async removeModule( 49 | moduleProtocol: string, 50 | modulePath: string, 51 | ): Promise { 52 | const dp = path.join( 53 | vendorDirectoryPath, 54 | moduleProtocol, 55 | modulePath, 56 | ); 57 | 58 | try { 59 | await Deno.remove(dp, { recursive: true }); 60 | } catch (e) { 61 | if (e instanceof Deno.errors.NotFound) { 62 | throw new Error(`module already removed: ${dp}`); 63 | } else { 64 | throw new Error(`failed to remove directory: ${dp}, ${e}`); 65 | } 66 | } 67 | } 68 | 69 | async addLink( 70 | moduleProtocol: string, 71 | modulePath: string, 72 | moduleVersion: string, 73 | filePath: string, 74 | hasDefaultExport: boolean, 75 | ): Promise { 76 | const directoryPath = path.dirname(filePath); 77 | const fp = path.join( 78 | vendorDirectoryPath, 79 | moduleProtocol, 80 | modulePath, 81 | filePath, 82 | ); 83 | const dp = path.join( 84 | vendorDirectoryPath, 85 | moduleProtocol, 86 | modulePath, 87 | directoryPath, 88 | ); 89 | const url = createURL(moduleProtocol, modulePath, moduleVersion, filePath); 90 | let script = sprintf('export * from "%s";\n', url); 91 | if (hasDefaultExport) { 92 | script += sprintf('export { default } from "%s";\n', url); 93 | } 94 | 95 | // create directories and file 96 | await Deno.mkdir(dp, { recursive: true }); 97 | await Deno.writeFile(fp, enc.encode(script)); 98 | } 99 | 100 | async removeLink( 101 | moduleProtocol: string, 102 | modulePath: string, 103 | filePath: string, 104 | ): Promise { 105 | const fp = path.join( 106 | vendorDirectoryPath, 107 | moduleProtocol, 108 | modulePath, 109 | filePath, 110 | ); 111 | 112 | try { 113 | await Deno.remove(fp, { recursive: false }); 114 | } catch (e) { 115 | if (e instanceof Deno.errors.NotFound) { 116 | throw new Error(`link already removed: ${fp}`); 117 | } else { 118 | throw new Error(`failed to remove directory: ${fp}, ${e}`); 119 | } 120 | } 121 | } 122 | 123 | async addAlias( 124 | moduleProtocol: string, 125 | modulePath: string, 126 | filePath: string, 127 | aliasPath: string, 128 | hasDefaultExport: boolean, 129 | ): Promise { 130 | const aliasDirectoryPath = path.dirname(aliasPath); 131 | const fp = path.join( 132 | vendorDirectoryPath, 133 | aliasPath, 134 | ); 135 | const dp = path.join( 136 | vendorDirectoryPath, 137 | aliasDirectoryPath, 138 | ); 139 | const aliasTargetPath = `./${moduleProtocol}/${modulePath}${filePath}`; 140 | let script = sprintf('export * from "%s";\n', aliasTargetPath); 141 | if (hasDefaultExport) { 142 | script += sprintf('export { default } from "%s";\n', aliasTargetPath); 143 | } 144 | 145 | // create directories and file 146 | await Deno.mkdir(dp, { recursive: true }); 147 | await Deno.writeFile(fp, enc.encode(script)); 148 | } 149 | 150 | async removeAlias(aliasPath: string): Promise { 151 | const fp = path.join( 152 | vendorDirectoryPath, 153 | aliasPath, 154 | ); 155 | 156 | try { 157 | await Deno.remove(fp, { recursive: false }); 158 | } catch (e) { 159 | if (e instanceof Deno.errors.NotFound) { 160 | throw new Error(`alias already removed: ${fp}`); 161 | } else { 162 | throw new Error(`failed to remove alias: ${fp}, ${e}`); 163 | } 164 | } 165 | } 166 | 167 | async updateLink( 168 | moduleProtocol: string, 169 | modulePath: string, 170 | moduleVersion: string, 171 | filePath: string, 172 | hasDefaultExport: boolean, 173 | ): Promise { 174 | const fp = path.join( 175 | vendorDirectoryPath, 176 | moduleProtocol, 177 | modulePath, 178 | filePath, 179 | ); 180 | const url = createURL(moduleProtocol, modulePath, moduleVersion, filePath); 181 | let script = sprintf('export * from "%s";\n', url); 182 | if (hasDefaultExport) { 183 | script += sprintf('export { default } from "%s";\n', url); 184 | } 185 | await Deno.writeFile(fp, enc.encode(script)); 186 | } 187 | 188 | async loadConfig(): Promise { 189 | const jsonBody = dec.decode(await Deno.readFile(this.filePath)); 190 | const configObj = JSON.parse(jsonBody); 191 | if (!configObj.aliases) { 192 | configObj.aliases = {}; 193 | } 194 | const config = configObj as Config; 195 | // throws error 196 | validateConfig(config); 197 | config.modules = config.modules.map( 198 | (mod) => new Module(mod.protocol, mod.path, mod.version, mod.files), 199 | ); 200 | return config; 201 | } 202 | 203 | async saveConfig(config: Config) { 204 | const jsonBody = JSON.stringify(config, undefined, 2); 205 | await Deno.writeFile(this.filePath, enc.encode(jsonBody)); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | const { cwd } = Deno; 2 | 3 | import { Module } from "./module.ts"; 4 | import { 5 | Action, 6 | ADD_ALIAS, 7 | ADD_LINK, 8 | ADD_MODULE, 9 | REMOVE_ALIAS, 10 | REMOVE_LINK, 11 | REMOVE_MODULE, 12 | UPDATE_MODULE, 13 | } from "./actions.ts"; 14 | import { mutateRepository, mutateStore } from "./mutations.ts"; 15 | import { Store } from "./store.ts"; 16 | import { Repository } from "./repository.ts"; 17 | import { getFormattedImportFilePaths } from "./ast.ts"; 18 | 19 | export class App { 20 | constructor(public store: Store, private repo: Repository) {} 21 | 22 | async init(): Promise { 23 | await this.repo.saveConfig({ 24 | modules: [], 25 | aliases: {}, 26 | }); 27 | console.log("successfully initialized dem.json"); 28 | } 29 | 30 | async addModule(urlStr: string): Promise { 31 | let module: Module; 32 | try { 33 | module = Module.parse(urlStr); 34 | } catch (e) { 35 | console.error(e.toString()); 36 | return; 37 | } 38 | 39 | try { 40 | await this.commit([ 41 | { 42 | type: ADD_MODULE, 43 | payload: { module }, 44 | }, 45 | ]); 46 | } catch (e) { 47 | console.error(e.toString()); 48 | return; 49 | } 50 | 51 | console.log( 52 | `successfully added new module: ${module.toString()}, version: ${module.version}`, 53 | ); 54 | } 55 | 56 | async removeModule(urlStr: string): Promise { 57 | let module: Module; 58 | try { 59 | module = Module.parse(urlStr); 60 | } catch (e) { 61 | console.error(e.toString()); 62 | return; 63 | } 64 | 65 | try { 66 | await this.commit([ 67 | { 68 | type: REMOVE_MODULE, 69 | payload: { 70 | moduleProtocol: module.protocol, 71 | modulePath: module.path, 72 | }, 73 | }, 74 | ]); 75 | } catch (e) { 76 | console.error(e.toString()); 77 | return; 78 | } 79 | 80 | console.log(`successfully removed module: ${urlStr}`); 81 | } 82 | 83 | async addLink(urlStr: string): Promise { 84 | try { 85 | await this.commit([ 86 | { 87 | type: ADD_LINK, 88 | payload: { 89 | link: urlStr, 90 | }, 91 | }, 92 | ]); 93 | } catch (e) { 94 | console.error(e.toString()); 95 | return; 96 | } 97 | 98 | console.log(`successfully created link: ${urlStr}`); 99 | } 100 | 101 | async removeLink(urlStr: string): Promise { 102 | try { 103 | await this.commit([ 104 | { 105 | type: REMOVE_LINK, 106 | payload: { 107 | link: urlStr, 108 | }, 109 | }, 110 | ]); 111 | } catch (e) { 112 | console.error(e.toString()); 113 | return; 114 | } 115 | 116 | console.log(`successfully removed link: ${urlStr}`); 117 | } 118 | 119 | async addAlias(aliasTargetPath: string, aliasPath: string): Promise { 120 | try { 121 | await this.commit([ 122 | { 123 | type: ADD_ALIAS, 124 | payload: { 125 | aliasPath, 126 | aliasTargetPath, 127 | }, 128 | }, 129 | ]); 130 | } catch (e) { 131 | console.error(e.toString()); 132 | return; 133 | } 134 | 135 | console.log( 136 | `successfully created alias: ${aliasPath} => ${aliasTargetPath}`, 137 | ); 138 | } 139 | 140 | async removeAlias(aliasPath: string): Promise { 141 | try { 142 | await this.commit([ 143 | { 144 | type: REMOVE_ALIAS, 145 | payload: { 146 | aliasPath, 147 | }, 148 | }, 149 | ]); 150 | } catch (e) { 151 | console.error(e.toString()); 152 | return; 153 | } 154 | 155 | console.log(`successfully removed alias: ${aliasPath}`); 156 | } 157 | 158 | async updateModule(urlStr: string): Promise { 159 | const updatedMod = Module.parse(urlStr); 160 | if (!updatedMod) { 161 | console.error(`failed to parse module: ${urlStr}`); 162 | return; 163 | } 164 | 165 | try { 166 | await this.commit([ 167 | { 168 | type: UPDATE_MODULE, 169 | payload: { 170 | moduleProtocol: updatedMod.protocol, 171 | modulePath: updatedMod.path, 172 | moduleVersion: updatedMod.version, 173 | }, 174 | }, 175 | ]); 176 | } catch (e) { 177 | console.error(e.toString()); 178 | return; 179 | } 180 | 181 | console.log( 182 | `successfully updated module: ${updatedMod.toString()}, version: ${updatedMod.version}`, 183 | ); 184 | } 185 | 186 | async ensure(excludes: string[]): Promise { 187 | const imports = await getFormattedImportFilePaths(cwd(), excludes); 188 | const actions: Action[] = []; 189 | for (const urlStr of imports) { 190 | let link = urlStr; 191 | if (this.store.config.aliases[urlStr]) { 192 | link = this.store.config.aliases[urlStr]; 193 | } 194 | actions.push({ 195 | type: ADD_LINK, 196 | payload: { 197 | link, 198 | }, 199 | }); 200 | } 201 | 202 | await this.commit(actions); 203 | console.log(`succeeded to resolve modules`); 204 | } 205 | 206 | async prune(excludes: string[]): Promise { 207 | const imports = await getFormattedImportFilePaths(cwd(), excludes); 208 | 209 | // Detect removed links 210 | const removedLinks: string[] = []; 211 | const aliasValues = Object.values(this.store.config.aliases); 212 | for (const mod of this.store.config.modules) { 213 | for (const filePath of mod.files) { 214 | const modUrlStr = `${mod.protocol}://${mod.path}${filePath}`; 215 | if (!imports.includes(modUrlStr) && !aliasValues.includes(modUrlStr)) { 216 | removedLinks.push(modUrlStr); 217 | } 218 | } 219 | } 220 | 221 | const removeLinkActions: Action[] = []; 222 | // Remove links 223 | for (const link of removedLinks) { 224 | removeLinkActions.push({ 225 | type: REMOVE_LINK, 226 | payload: { 227 | link, 228 | }, 229 | }); 230 | } 231 | 232 | try { 233 | this.store = mutateStore(this.store, removeLinkActions); 234 | } catch (e) { 235 | console.error(e.stack); 236 | return; 237 | } 238 | 239 | const removeModuleActions: Action[] = []; 240 | for (const mod of this.store.config.modules) { 241 | if (mod.files.length === 0) { 242 | removeModuleActions.push({ 243 | type: REMOVE_MODULE, 244 | payload: { 245 | moduleProtocol: mod.protocol, 246 | modulePath: mod.path, 247 | }, 248 | }); 249 | } 250 | } 251 | 252 | try { 253 | this.store = mutateStore(this.store, removeModuleActions); 254 | } catch (e) { 255 | console.error(e.stack); 256 | return; 257 | } 258 | 259 | const actions: Action[] = [...removeLinkActions, ...removeModuleActions]; 260 | 261 | try { 262 | await mutateRepository(this.repo, this.store, actions); 263 | } catch (e) { 264 | console.error(e.stack); 265 | return; 266 | } 267 | 268 | await this.repo.saveConfig(this.store.config); 269 | console.log(`succeeded to prune modules`); 270 | } 271 | 272 | async commit(actions: Action[]) { 273 | try { 274 | this.store = mutateStore(this.store, actions); 275 | } catch (e) { 276 | console.error(e.stack); 277 | } 278 | try { 279 | await mutateRepository(this.repo, this.store, actions); 280 | } catch (e) { 281 | console.error(e.stack); 282 | } 283 | await this.repo.saveConfig(this.store.config); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /mutations.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Action, 3 | ADD_ALIAS, 4 | ADD_LINK, 5 | ADD_MODULE, 6 | REMOVE_ALIAS, 7 | REMOVE_LINK, 8 | REMOVE_MODULE, 9 | UPDATE_MODULE, 10 | } from "./actions.ts"; 11 | import { duplicateStore, Store } from "./store.ts"; 12 | import { compareModules, Module, moduleEquals } from "./module.ts"; 13 | import { Repository } from "./repository.ts"; 14 | import { createURL } from "./net.ts"; 15 | import * as path from "./vendor/https/deno.land/std/path/mod.ts"; 16 | import { hasDefaultExportLocal, hasDefaultExportRemote } from "./ast.ts"; 17 | 18 | const vendorDirectoryPath = "vendor"; 19 | 20 | // mutateStore mutates store. This affects only for memory. 21 | export function mutateStore(store: Store, actions: Action[]): Store { 22 | const s = duplicateStore(store); 23 | const { modules } = s.config; 24 | const aliases = { ...s.config.aliases }; 25 | for (const action of actions) { 26 | switch (action.type) { 27 | case ADD_MODULE: { 28 | const { module } = action.payload; 29 | for (const mod of modules) { 30 | if (moduleEquals(mod, module)) { 31 | throw new Error(`module already exists: ${module.toString()} 32 | to update module, please use 'dem update'.`); 33 | } 34 | } 35 | modules.push(module); 36 | break; 37 | } 38 | 39 | case REMOVE_MODULE: { 40 | const { moduleProtocol, modulePath } = action.payload; 41 | const urlStr = `${moduleProtocol}://${modulePath}`; 42 | let foundModIndex = 0; 43 | let foundMod: Module | undefined; 44 | for (const mod of modules) { 45 | if (urlStr.startsWith(mod.toString())) { 46 | foundMod = mod; 47 | break; 48 | } 49 | foundModIndex++; 50 | } 51 | if (!foundMod) { 52 | throw new Error(`remove module: module not found for: ${urlStr}`); 53 | } 54 | modules.splice(foundModIndex, 1); 55 | break; 56 | } 57 | 58 | case ADD_LINK: { 59 | const { link } = action.payload; 60 | const foundMod = modules.find((mod) => { 61 | return link.startsWith(mod.toString()); 62 | }); 63 | if (!foundMod) { 64 | throw new Error(`add link: module not found for: ${link}`); 65 | } 66 | const filePath = link.split(foundMod.toString())[1]; 67 | if (foundMod.files.includes(filePath)) { 68 | break; 69 | } 70 | foundMod.files.push(filePath); 71 | break; 72 | } 73 | 74 | case REMOVE_LINK: { 75 | const { link } = action.payload; 76 | const foundMod = modules.find((mod) => { 77 | return link.startsWith(mod.toString()); 78 | }); 79 | if (!foundMod) { 80 | throw new Error(`remove link: module not found for: ${link}`); 81 | } 82 | const filePath = link.split(foundMod.toString())[1]; 83 | const foundFileIndex = foundMod.files.indexOf(filePath); 84 | if (foundFileIndex < 0) { 85 | throw new Error(`file not linked: ${link}`); 86 | } 87 | foundMod.files.splice(foundFileIndex, 1); 88 | break; 89 | } 90 | 91 | case ADD_ALIAS: { 92 | const { aliasPath, aliasTargetPath } = action.payload; 93 | 94 | if (aliases[aliasPath]) { 95 | throw new Error( 96 | `alias already exists for: ${aliasPath}. please execute dem unalias before this.`, 97 | ); 98 | } 99 | 100 | const foundMod = modules.find((mod) => { 101 | return aliasTargetPath.startsWith(mod.toString()); 102 | }); 103 | if (!foundMod) { 104 | throw new Error( 105 | `add alias: module not found for: ${aliasTargetPath}`, 106 | ); 107 | } 108 | 109 | aliases[aliasPath] = aliasTargetPath; 110 | break; 111 | } 112 | 113 | case REMOVE_ALIAS: { 114 | const { aliasPath } = action.payload; 115 | if (!aliases[aliasPath]) { 116 | throw new Error(`alias does not exist for: ${aliasPath}.`); 117 | } 118 | delete aliases[aliasPath]; 119 | break; 120 | } 121 | 122 | case UPDATE_MODULE: { 123 | const { moduleProtocol, modulePath, moduleVersion } = action.payload; 124 | const urlStr = `${moduleProtocol}://${modulePath}`; 125 | const foundMod = modules.find((mod) => { 126 | return mod.protocol === moduleProtocol && mod.path === modulePath; 127 | }); 128 | if (!foundMod) { 129 | throw new Error(`update module: module not found for: ${urlStr}`); 130 | } 131 | foundMod.version = moduleVersion; 132 | break; 133 | } 134 | } 135 | } 136 | 137 | // sort modules 138 | modules.sort(compareModules); 139 | for (const mod of modules) { 140 | mod.files.sort(); 141 | } 142 | 143 | // sort aliases 144 | const aliasesEntries = Object.entries(aliases); 145 | aliasesEntries.sort((a, b) => { 146 | return a[0].localeCompare(b[0]); 147 | }); 148 | s.config.aliases = Object.fromEntries(aliasesEntries); 149 | 150 | return s; 151 | } 152 | 153 | // mutateRepository mutates files controlled by repository. 154 | // This persists mutated files. 155 | export async function mutateRepository( 156 | repository: Repository, 157 | store: Store, 158 | actions: Action[], 159 | ) { 160 | const { modules } = store.config; 161 | for (const action of actions) { 162 | switch (action.type) { 163 | case ADD_MODULE: { 164 | // Do nothing for add module. 165 | break; 166 | } 167 | 168 | case REMOVE_MODULE: { 169 | const { moduleProtocol, modulePath } = action.payload; 170 | await repository.removeModule(moduleProtocol, modulePath); 171 | break; 172 | } 173 | 174 | case ADD_LINK: { 175 | const { link } = action.payload; 176 | // find module 177 | const foundMod = modules.find((mod) => { 178 | return link.startsWith(mod.toString()); 179 | }); 180 | if (!foundMod) { 181 | throw new Error(`add link: module not found for: ${link}`); 182 | } 183 | 184 | const filePath = link.split(foundMod.toString())[1]; 185 | const url = createURL( 186 | foundMod.protocol, 187 | foundMod.path, 188 | foundMod.version, 189 | filePath, 190 | ); 191 | const hasDefault = await hasDefaultExportRemote(url); 192 | 193 | await repository.addLink( 194 | foundMod.protocol, 195 | foundMod.path, 196 | foundMod.version, 197 | filePath, 198 | hasDefault, 199 | ); 200 | break; 201 | } 202 | 203 | case REMOVE_LINK: { 204 | const { link } = action.payload; 205 | const foundMod = modules.find((mod) => { 206 | return link.startsWith(mod.toString()); 207 | }); 208 | if (!foundMod) { 209 | throw new Error(`remove link: module not found for: ${link}`); 210 | } 211 | 212 | // create file path 213 | const filePath = link.split(foundMod.toString())[1]; 214 | await repository.removeLink(foundMod.protocol, foundMod.path, filePath); 215 | break; 216 | } 217 | 218 | case ADD_ALIAS: { 219 | const { aliasPath, aliasTargetPath } = action.payload; 220 | 221 | const foundMod = modules.find((mod) => { 222 | return aliasTargetPath.startsWith(mod.toString()); 223 | }); 224 | if (!foundMod) { 225 | throw new Error( 226 | `add alias: module not found for: ${aliasTargetPath}`, 227 | ); 228 | } 229 | 230 | const linkedFilePath = aliasTargetPath.split(foundMod.toString())[1]; 231 | 232 | const fp = path.join( 233 | vendorDirectoryPath, 234 | foundMod.protocol, 235 | foundMod.path, 236 | linkedFilePath, 237 | ); 238 | 239 | let hasDefault = false; 240 | try { 241 | hasDefault = await hasDefaultExportLocal(fp); 242 | } catch (e) { 243 | if (!(e instanceof Deno.errors.NotFound)) { 244 | throw e; 245 | } 246 | // let has default as false 247 | } 248 | 249 | await repository.addAlias( 250 | foundMod.protocol, 251 | foundMod.path, 252 | linkedFilePath, 253 | aliasPath, 254 | hasDefault, 255 | ); 256 | break; 257 | } 258 | 259 | case REMOVE_ALIAS: { 260 | const { aliasPath } = action.payload; 261 | await repository.removeAlias(aliasPath); 262 | break; 263 | } 264 | 265 | case UPDATE_MODULE: { 266 | const { moduleProtocol, modulePath, moduleVersion } = action.payload; 267 | const urlStr = `${moduleProtocol}://${modulePath}`; 268 | const foundMod = modules.find((mod) => { 269 | return mod.protocol === moduleProtocol && mod.path === modulePath; 270 | }); 271 | if (!foundMod) { 272 | throw new Error(`update module: module not found for: ${urlStr}`); 273 | } 274 | 275 | for (const filePath of foundMod.files) { 276 | const url = createURL( 277 | moduleProtocol, 278 | modulePath, 279 | moduleVersion, 280 | filePath, 281 | ); 282 | const hasDefault = await hasDefaultExportRemote(url); 283 | 284 | await repository.updateLink( 285 | moduleProtocol, 286 | modulePath, 287 | moduleVersion, 288 | filePath, 289 | hasDefault, 290 | ); 291 | } 292 | break; 293 | } 294 | } 295 | } 296 | } 297 | --------------------------------------------------------------------------------