├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── FUNDING.yml ├── .gitignore ├── .lintstagedrc.yml ├── .prettierrc.json ├── README.md ├── api ├── _utils │ ├── extract-http-error.ts │ ├── http_status_code.ts │ └── pkg.ts ├── ping.ts ├── pkg-from-url.ts └── pkg.ts ├── docs ├── .vuepress │ ├── .eslintrc.js │ ├── .lintstagedrc.yml │ ├── .npmignore │ ├── api-from-url.js │ ├── components │ │ ├── GitpkgApi.vue │ │ └── ServiceWorkerPopup.vue │ ├── config.js │ ├── enhanceApp.js │ ├── gen │ │ └── site-meta.json │ ├── install-command.js │ ├── my-components │ │ ├── ActionBar.vue │ │ ├── ApiChoicesDisplay.vue │ │ ├── ButtonGroup.vue │ │ ├── CopyText.vue │ │ ├── CustomScriptEdit.vue │ │ ├── CustomScripts.vue │ │ ├── PmIcon.vue │ │ ├── SelectDropdown.vue │ │ └── SingleApiDisplay.vue │ ├── package.json │ ├── public │ │ ├── cover.svg │ │ ├── favicon.svg │ │ ├── icon.svg │ │ ├── icons │ │ │ ├── apple-icon-120.png │ │ │ ├── apple-icon-152.png │ │ │ ├── apple-icon-167.png │ │ │ ├── apple-icon-180.png │ │ │ ├── favicon-196.png │ │ │ ├── manifest-icon-192.png │ │ │ └── manifest-icon-512.png │ │ └── manifest.webmanifest │ ├── scripts │ │ ├── constants.js │ │ ├── gen-assets.js │ │ ├── main.js │ │ └── manifest.webmanifest │ ├── styles │ │ ├── index.styl │ │ ├── palette.styl │ │ └── select.styl │ └── yarn-svg-content.js ├── README.md └── guide │ └── README.md ├── lerna.json ├── now.json ├── package.json ├── packages └── core │ ├── .eslintignore │ ├── .eslintrc.js │ ├── .lintstagedrc.yml │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── api │ │ ├── codeload-url.ts │ │ ├── download-git-pkg.ts │ │ └── index.ts │ ├── index.ts │ ├── parse-url-query │ │ ├── default-parser.ts │ │ ├── error.ts │ │ ├── get-value.ts │ │ ├── index.ts │ │ ├── parser.ts │ │ └── plugins │ │ │ ├── custom-scripts │ │ │ ├── index.ts │ │ │ ├── parse-query.spec.ts │ │ │ ├── parse-query.ts │ │ │ └── plugin.ts │ │ │ ├── index.ts │ │ │ └── url-and-commit │ │ │ ├── commit-ish.ts │ │ │ ├── from-query.ts │ │ │ ├── from-url.ts │ │ │ ├── index.ts │ │ │ └── plugin.ts │ └── tar │ │ ├── custom-scripts.spec.ts │ │ ├── custom-scripts.ts │ │ ├── extract-sub-folder.spec.ts │ │ ├── extract-sub-folder.ts │ │ ├── index.ts │ │ ├── modify-single-file.ts │ │ ├── prepend-path.spec.ts │ │ └── prepend-path.ts │ ├── test │ └── util │ │ └── tar-entry.ts │ ├── tsconfig.json │ └── tsconfig.prod.json ├── tools └── common │ ├── .eslintrc.js │ ├── .lintstagedrc.yml │ ├── babel │ └── index.js │ ├── eslint │ ├── index.js │ └── node.js │ ├── jest │ └── index.js │ ├── package.json │ └── tsconfig.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /.now 2 | /public 3 | /packages 4 | /tools 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@gitpkg/common/eslint")(__dirname); 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: gitpkg 6 | ko_fi: equalma 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eslintcache 2 | .now 3 | /public 4 | dist 5 | 6 | .DS_Store 7 | node_modules 8 | /dist 9 | 10 | # local env files 11 | .env.local 12 | .env.*.local 13 | .env 14 | .env.build 15 | 16 | # Log files 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # Editor directories and files 22 | .idea 23 | .vscode 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | -------------------------------------------------------------------------------- /.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "*.{md,html,css,yml,yaml,json}": "prettier --write" 2 | ".eslintrc.js": "prettier --write" 3 | "./!(.eslintrc).{js,ts}": "yarn run lint:fix" 4 | "api/*.{js,ts}": "yarn run lint:fix" 5 | "*.{png,jpeg,jpg,gif,svg}": "imagemin-lint-staged" 6 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitPkg-icon 2 | 3 | # GitPkg 4 | 5 | [![GitHub deployments](https://img.shields.io/github/deployments/EqualMa/gitpkg/production?label=gitpkg.now.sh&logo=zeit&style=flat-square)](https://gitpkg.now.sh) 6 | [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg?style=flat-square)](https://lerna.js.org/) 7 | 8 | GitPkg enables you to use a sub directory in a github repo as yarn / npm dependency. 9 | 10 | [:tada: Try Now !](https://gitpkg.now.sh) 11 | 12 | :unicorn: Features: 13 | 14 | - sub folder of a github repo as yarn / npm dependency 15 | - use [custom scripts](https://gitpkg.now.sh/guide/#custom-scripts) to build source code when installing 16 | -------------------------------------------------------------------------------- /api/_utils/extract-http-error.ts: -------------------------------------------------------------------------------- 1 | import { HTTPError } from "got"; 2 | 3 | export interface HttpErrorInfo { 4 | code: number; 5 | message: string; 6 | } 7 | 8 | export function extractInfoFromHttpError( 9 | err: unknown, 10 | defaults: HttpErrorInfo, 11 | ) { 12 | if (err instanceof HTTPError) { 13 | const code = 14 | (err.code && parseInt(err.code)) || 15 | err.response.statusCode || 16 | defaults.code; 17 | const message = err.message; 18 | return { code, message }; 19 | } else return defaults; 20 | } 21 | -------------------------------------------------------------------------------- /api/_utils/http_status_code.ts: -------------------------------------------------------------------------------- 1 | export const BAD_REQUEST = 400; 2 | export const INTERNAL_SERVER_ERROR = 500; 3 | -------------------------------------------------------------------------------- /api/_utils/pkg.ts: -------------------------------------------------------------------------------- 1 | import * as codes from "./http_status_code"; 2 | import { pipelineToDownloadGitPkg, getDefaultParser } from "@gitpkg/core"; 3 | import * as stream from "stream"; 4 | import { promisify } from "util"; 5 | import { extractInfoFromHttpError } from "./extract-http-error"; 6 | 7 | const pipeline = promisify(stream.pipeline); 8 | 9 | export interface PkgToResponseOptions { 10 | requestUrl: string | undefined; 11 | query: import("@now/node").NowRequestQuery; 12 | parseFromUrl: boolean; 13 | response: import("@now/node").NowResponse; 14 | } 15 | 16 | export async function pkg({ 17 | requestUrl, 18 | query, 19 | parseFromUrl, 20 | response, 21 | }: PkgToResponseOptions) { 22 | try { 23 | const pkgOpts = getDefaultParser(parseFromUrl).parse( 24 | requestUrl || "", 25 | query, 26 | ); 27 | const { commitIshInfo: cii } = pkgOpts; 28 | 29 | response.status(200); 30 | response.setHeader( 31 | "Content-Disposition", 32 | `attachment; filename="${[ 33 | cii.user, 34 | cii.repo, 35 | ...(cii.subdirs || []), 36 | cii.commit, 37 | ] 38 | .filter(Boolean) 39 | .join("-")}.tgz"`, 40 | ); 41 | response.setHeader("Content-Type", "application/gzip"); 42 | await pipeline([...pipelineToDownloadGitPkg(pkgOpts), response]); 43 | } catch (err) { 44 | console.error(`request ${requestUrl} fail with message: ${err.message}`); 45 | const { code, message } = extractInfoFromHttpError(err, { 46 | code: codes.INTERNAL_SERVER_ERROR, 47 | message: `download or parse fail for: ${requestUrl}`, 48 | }); 49 | response.removeHeader("Content-Disposition"); 50 | response.removeHeader("Content-Type"); 51 | response.status(code).json(message); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /api/ping.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from "@now/node"; 2 | 3 | export default (request: NowRequest, response: NowResponse) => { 4 | const { name = "World" } = request.query; 5 | response.status(200).json({ 6 | msg: `Hello ${name} at timestamp ${new Date().getTime()}`, 7 | query: request.query, 8 | url: request.url, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /api/pkg-from-url.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from "@now/node"; 2 | import { pkg } from "./_utils/pkg"; 3 | 4 | export default async (request: NowRequest, response: NowResponse) => { 5 | await pkg({ 6 | query: request.query, 7 | requestUrl: request.url, 8 | parseFromUrl: true, 9 | response, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /api/pkg.ts: -------------------------------------------------------------------------------- 1 | import { NowRequest, NowResponse } from "@now/node"; 2 | import { pkg } from "./_utils/pkg"; 3 | 4 | export default async (request: NowRequest, response: NowResponse) => { 5 | await pkg({ 6 | query: request.query, 7 | requestUrl: request.url, 8 | parseFromUrl: false, 9 | response, 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /docs/.vuepress/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:prettier/recommended", 6 | "plugin:node/recommended-module", 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | }, 11 | overrides: [ 12 | { 13 | files: ["./config.js", "scripts/**/*.js"], 14 | extends: [ 15 | "eslint:recommended", 16 | "plugin:prettier/recommended", 17 | "plugin:node/recommended-script", 18 | ], 19 | rules: { 20 | "node/no-extraneous-require": [ 21 | "error", 22 | { 23 | allowModules: ["rimraf"], 24 | }, 25 | ], 26 | }, 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /docs/.vuepress/.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "*.vue": "prettier --write" 2 | "!(.eslintrc).js": "yarn run lint:fix" 3 | -------------------------------------------------------------------------------- /docs/.vuepress/.npmignore: -------------------------------------------------------------------------------- 1 | # This file is used to disable 2 | # https://github.com/mysticatea/eslint-plugin-node/blob/HEAD/docs/rules/no-unpublished-require.md 3 | # for build scripts 4 | 5 | /config.js 6 | /scripts 7 | -------------------------------------------------------------------------------- /docs/.vuepress/api-from-url.js: -------------------------------------------------------------------------------- 1 | const branchNamePrecedence = [ 2 | "master", 3 | "dev", 4 | "next", 5 | // tag vx.x.x 6 | name => /[vV][0-9.]+.*/.test(name), 7 | "bug/", 8 | "feat/", 9 | "fix/", 10 | ]; 11 | 12 | const getPrecedence = ({ commit }) => { 13 | let i = branchNamePrecedence.findIndex(v => 14 | typeof v === "function" ? v(commit) : commit === v, 15 | ); 16 | 17 | if (i === -1) { 18 | i = branchNamePrecedence.findIndex( 19 | v => typeof v === "string" && commit.startsWith(v), 20 | ); 21 | if (i !== -1) i += 10000; 22 | } 23 | 24 | return i === -1 ? Infinity : i; 25 | }; 26 | 27 | const API_BASE = "https://gitpkg.now.sh/"; 28 | const REGEX_URL = /^https?:\/\/([^/?#]+)\/([^/?#]+)\/([^/?#]+)(?:(?:\/tree\/([^#?]+))|\/)?([#?].*)?$/; 29 | 30 | function customScriptToQueryParam(customScript) { 31 | const { script, name, type } = customScript; 32 | 33 | const r = /(^\s*&&\s*)|(\s*&&\s*$)/g; 34 | let normScript = script.replace(r, ""); 35 | switch (type) { 36 | case "prepend": 37 | normScript = normScript + " &&"; 38 | break; 39 | case "append": 40 | normScript = "&& " + normScript; 41 | break; 42 | } 43 | 44 | return ( 45 | "scripts." + encodeURIComponent(name) + "=" + encodeURIComponent(normScript) 46 | ); 47 | } 48 | 49 | function queryStringOf(commit, customScripts) { 50 | // postinstall=echo%20gitpkg&build=echo%20building 51 | const csPart = customScripts 52 | .map(cs => customScriptToQueryParam(cs)) 53 | .join("&"); 54 | 55 | if (!csPart) return commit ? "?" + commit : ""; 56 | else return "?" + (commit || "master") + "&" + csPart; 57 | } 58 | 59 | function apiFromCommitInfo( 60 | { commit, subdir, originalUrl, domain, userName, repoName }, 61 | customScripts, 62 | ) { 63 | customScripts = customScripts.filter(cs => cs.name && cs.script); 64 | 65 | const repo = userName + "/" + repoName; 66 | 67 | const data = { 68 | originalUrl, 69 | domain, 70 | userName, 71 | repoName, 72 | commit, 73 | subdir, 74 | }; 75 | 76 | const commitPart = queryStringOf(commit, customScripts); 77 | 78 | if (!subdir) { 79 | if (customScripts.length === 0) { 80 | return { 81 | type: "warn", 82 | warnType: "suggest-to-use", 83 | data, 84 | suggestion: { 85 | apiUrl: commit ? repo + "#" + commit : repo, 86 | }, 87 | apiUrl: API_BASE + repo + commitPart, 88 | params: { url: repo, commit }, 89 | }; 90 | } else { 91 | return { 92 | type: "success", 93 | data, 94 | apiUrl: API_BASE + repo + commitPart, 95 | params: { url: repo, commit }, 96 | }; 97 | } 98 | } else { 99 | return { 100 | type: "success", 101 | data, 102 | apiUrl: API_BASE + repo + "/" + subdir + commitPart, 103 | params: { url: repo + "/" + subdir, commit }, 104 | }; 105 | } 106 | } 107 | 108 | export const apiFromUrl = (url, customScripts) => { 109 | const match = REGEX_URL.exec(url.trim()); 110 | if (!match) { 111 | return { 112 | type: "error", 113 | errorType: "url-wrong", 114 | data: { 115 | originalUrl: url, 116 | }, 117 | }; 118 | } 119 | 120 | const [fullUrl, domain, userName, repoName, _commitAndSubDir] = match; 121 | 122 | const data = { 123 | originalUrl: fullUrl, 124 | domain, 125 | userName, 126 | repoName, 127 | }; 128 | 129 | if (domain !== "github.com") { 130 | return { 131 | type: "error", 132 | errorType: "platform-not-supported", 133 | data, 134 | }; 135 | } 136 | 137 | const commitAndSubDir = 138 | _commitAndSubDir && _commitAndSubDir.endsWith("/") 139 | ? _commitAndSubDir.slice(0, -1) 140 | : _commitAndSubDir; 141 | 142 | // no sub folder 143 | // || commitAndSubDir.indexOf("/") === -1 144 | if (!commitAndSubDir) { 145 | return apiFromCommitInfo( 146 | { 147 | ...data, 148 | commit: undefined, 149 | subdir: undefined, 150 | }, 151 | customScripts, 152 | ); 153 | } 154 | 155 | const routes = commitAndSubDir.split("/").filter(Boolean); 156 | 157 | const possibleCommitAndSubDirs = routes.map((p, i, arr) => { 158 | return { 159 | commit: arr.slice(0, i + 1), 160 | subdir: arr.slice(i + 1), 161 | }; 162 | }); 163 | 164 | return { 165 | type: "choice", 166 | data, 167 | possibleApis: possibleCommitAndSubDirs 168 | .map(p => { 169 | const subdir = p.subdir.join("/"); 170 | const commit = p.commit.join("/"); 171 | return apiFromCommitInfo({ ...data, subdir, commit }, customScripts); 172 | }) 173 | .map(info => ({ 174 | info, 175 | precedence: getPrecedence({ 176 | commit: info.data.commit, 177 | subdir: info.data.subdir, 178 | }), 179 | })) 180 | .sort((a, b) => a.precedence - b.precedence) 181 | .map(a => a.info), 182 | }; 183 | }; 184 | -------------------------------------------------------------------------------- /docs/.vuepress/components/GitpkgApi.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 84 | 85 | 101 | -------------------------------------------------------------------------------- /docs/.vuepress/components/ServiceWorkerPopup.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 16 | 42 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { SITE_META_FILE, PATH_DEST } = require("./scripts/constants"); 4 | const fs = require("fs").promises; 5 | const promiseGenerated = fs 6 | .readFile(SITE_META_FILE, "utf-8") 7 | .then(str => JSON.parse(str)); 8 | 9 | module.exports = async () => { 10 | const { head, name, description } = await promiseGenerated; 11 | 12 | return { 13 | title: name, 14 | description, 15 | themeConfig: { 16 | nav: [ 17 | { text: "Home", link: "/" }, 18 | { text: "Guide", link: "/guide/" }, 19 | { text: "GitHub", link: "https://github.com/EqualMa/gitpkg" }, 20 | ], 21 | }, 22 | dest: PATH_DEST, 23 | head: [ 24 | ...head, 25 | ["link", { rel: "manifest", href: "/manifest.webmanifest" }], 26 | ["meta", { name: "theme-color", content: "#F06292" }], 27 | [ 28 | "meta", 29 | { name: "apple-mobile-web-app-status-bar-style", content: "default" }, 30 | ], 31 | ["link", { rel: "mask-icon", href: "/favicon.svg", color: "#ffffff" }], 32 | ["meta", { name: "msapplication-TileImage", content: "/icon.svg" }], 33 | ["meta", { name: "msapplication-TileColor", content: "#F06292" }], 34 | ].filter(Boolean), 35 | plugins: [ 36 | [ 37 | // https://vuepress.vuejs.org/plugin/official/plugin-pwa.html#vuepress-plugin-pwa 38 | "@vuepress/pwa", 39 | { 40 | serviceWorker: true, 41 | popupComponent: "ServiceWorkerPopup", 42 | updatePopup: true, 43 | generateSWConfig: { 44 | importWorkboxFrom: "local", 45 | }, 46 | }, 47 | ], 48 | ].filter(Boolean), 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | import "normalize.css/normalize.css"; 2 | import VueClipboard from "vue-clipboard2"; 3 | 4 | // https://vuepress.vuejs.org/guide/basic-config.html#app-level-enhancements 5 | export default ({ Vue }) => { 6 | Vue.use(VueClipboard); 7 | }; 8 | -------------------------------------------------------------------------------- /docs/.vuepress/gen/site-meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "head": [ 3 | [ 4 | "link", 5 | { 6 | "rel": "icon", 7 | "type": "image/png", 8 | "sizes": "196x196", 9 | "href": "icons/favicon-196.png" 10 | } 11 | ], 12 | ["meta", { "name": "apple-mobile-web-app-capable", "content": "yes" }], 13 | [ 14 | "link", 15 | { 16 | "rel": "apple-touch-icon", 17 | "sizes": "180x180", 18 | "href": "icons/apple-icon-180.png" 19 | } 20 | ], 21 | [ 22 | "link", 23 | { 24 | "rel": "apple-touch-icon", 25 | "sizes": "167x167", 26 | "href": "icons/apple-icon-167.png" 27 | } 28 | ], 29 | [ 30 | "link", 31 | { 32 | "rel": "apple-touch-icon", 33 | "sizes": "152x152", 34 | "href": "icons/apple-icon-152.png" 35 | } 36 | ], 37 | [ 38 | "link", 39 | { 40 | "rel": "apple-touch-icon", 41 | "sizes": "120x120", 42 | "href": "icons/apple-icon-120.png" 43 | } 44 | ] 45 | ], 46 | "name": "GitPkg", 47 | "description": "Using sub folders of a repo as yarn/npm dependencies made easy" 48 | } 49 | -------------------------------------------------------------------------------- /docs/.vuepress/install-command.js: -------------------------------------------------------------------------------- 1 | const managerAndCommands = { 2 | yarn: "yarn add", 3 | npm: "npm install", 4 | }; 5 | 6 | // yarn 7 | // https://classic.yarnpkg.com/en/docs/cli/add/ 8 | // npm 9 | // https://docs.npmjs.com/cli/install#synopsis 10 | const dependencyTypesAndArgs = { 11 | dependency: "", 12 | dev: "-D", 13 | peer: { 14 | yarn: "-P", 15 | npm: ({ url }) => `"{your-package-name}": ${JSON.stringify(url)}`, 16 | }, 17 | optional: "-O", 18 | // exact: "-E", 19 | // tilde: "-T", 20 | bundle: { npm: "-B" }, 21 | }; 22 | 23 | const managerNames = Object.keys(managerAndCommands); 24 | const dependencyTypes = Object.keys(dependencyTypesAndArgs); 25 | 26 | function getInstallCommand(managerName, dependencyType, url) { 27 | const command = managerAndCommands[managerName]; 28 | 29 | let depTypeArg = dependencyTypesAndArgs[dependencyType]; 30 | 31 | if (typeof depTypeArg === "object" && depTypeArg !== null) { 32 | depTypeArg = depTypeArg[managerName]; 33 | } 34 | 35 | if (typeof depTypeArg === "string") { 36 | return [command, depTypeArg, "'" + url + "'"].filter(Boolean).join(" "); 37 | } else if (typeof depTypeArg === "function") { 38 | return depTypeArg({ managerName, dependencyType, url }); 39 | } else if (typeof depTypeArg === "undefined" || depTypeArg === null) { 40 | return ""; 41 | } 42 | 43 | throw new Error( 44 | `Arg definition wrong for [Manager=${managerName} & DependencyType=${dependencyType}`, 45 | ); 46 | } 47 | 48 | export function installCommands(url) { 49 | const commands = Object.assign( 50 | {}, 51 | ...managerNames.map(m => ({ 52 | [m]: Object.assign( 53 | {}, 54 | ...dependencyTypes.map(d => ({ 55 | [d]: getInstallCommand(m, d, url), 56 | })), 57 | ), 58 | })), 59 | ); 60 | 61 | return { managerNames, dependencyTypes, commands }; 62 | } 63 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/ActionBar.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 31 | 32 | 62 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/ApiChoicesDisplay.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/ButtonGroup.vue: -------------------------------------------------------------------------------- 1 | 22 | 41 | 73 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/CopyText.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 74 | 75 | 79 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/CustomScriptEdit.vue: -------------------------------------------------------------------------------- 1 | 35 | 61 | 62 | 74 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/CustomScripts.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 77 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/PmIcon.vue: -------------------------------------------------------------------------------- 1 | 29 | 64 | 65 | 71 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/SelectDropdown.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | -------------------------------------------------------------------------------- /docs/.vuepress/my-components/SingleApiDisplay.vue: -------------------------------------------------------------------------------- 1 | 73 | 74 | 127 | 128 | 135 | -------------------------------------------------------------------------------- /docs/.vuepress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "engines": { 7 | "node": ">=12" 8 | }, 9 | "dependencies": { 10 | "mdi-vue": "^1.4.3", 11 | "normalize.css": "^8.0.1", 12 | "vue-clipboard2": "^0.3.1", 13 | "vue-select": "^3.9.5" 14 | }, 15 | "devDependencies": { 16 | "@vuepress/plugin-pwa": "^1.4.0", 17 | "parse5": "^5.1.1", 18 | "pwa-asset-generator": "^2.2.1", 19 | "vuepress": "^1.4.0" 20 | }, 21 | "scripts": { 22 | "precommit": "lint-staged", 23 | "lint": "eslint", 24 | "lint:fix": "eslint --cache --max-warnings 0 --fix", 25 | "gen-assets": "node scripts/main.js", 26 | "build": "yarn run docs:build", 27 | "docs:dev": "vuepress dev ../", 28 | "docs:build": "vuepress build ../" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/.vuepress/public/cover.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/.vuepress/public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-icon-120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/gitpkg/17394f4162651a4f77f94f816230b2202dd0a9e6/docs/.vuepress/public/icons/apple-icon-120.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-icon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/gitpkg/17394f4162651a4f77f94f816230b2202dd0a9e6/docs/.vuepress/public/icons/apple-icon-152.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-icon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/gitpkg/17394f4162651a4f77f94f816230b2202dd0a9e6/docs/.vuepress/public/icons/apple-icon-167.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/gitpkg/17394f4162651a4f77f94f816230b2202dd0a9e6/docs/.vuepress/public/icons/apple-icon-180.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/favicon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/gitpkg/17394f4162651a4f77f94f816230b2202dd0a9e6/docs/.vuepress/public/icons/favicon-196.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/manifest-icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/gitpkg/17394f4162651a4f77f94f816230b2202dd0a9e6/docs/.vuepress/public/icons/manifest-icon-192.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/manifest-icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel/gitpkg/17394f4162651a4f77f94f816230b2202dd0a9e6/docs/.vuepress/public/icons/manifest-icon-512.png -------------------------------------------------------------------------------- /docs/.vuepress/public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitPkg", 3 | "short_name": "GitPkg", 4 | "description": "Using sub folders of a repo as yarn/npm dependencies made easy", 5 | "background_color": "white", 6 | "categories": [ 7 | "utilities", 8 | "dev" 9 | ], 10 | "display": "standalone", 11 | "orientation": "portrait-primary", 12 | "scope": "./", 13 | "theme_color": "#F06292", 14 | "start_url": "./", 15 | "icons": [ 16 | { 17 | "src": "icons/manifest-icon-192.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "icons/manifest-icon-512.png", 23 | "sizes": "512x512", 24 | "type": "image/png" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /docs/.vuepress/scripts/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | exports.SITE_META_FILE = path.join(__dirname, "../gen/site-meta.json"); 5 | 6 | exports.PATH_DEST = path.join(__dirname, "../../../public"); 7 | -------------------------------------------------------------------------------- /docs/.vuepress/scripts/gen-assets.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { generateImages } = require("pwa-asset-generator"); 4 | const path = require("path"); 5 | const fs = require("fs").promises; 6 | const rimraf = require("rimraf"); 7 | const parse5 = require("parse5"); 8 | 9 | const PATH_PUBLIC = path.join(__dirname, "../docs/.vuepress/public/"); 10 | const PATH_BASE_MANIFEST = path.join(__dirname, "manifest.webmanifest"); 11 | const PATH_MANIFEST = path.join(PATH_PUBLIC, "manifest.webmanifest"); 12 | const PATH_ICON = path.join(PATH_PUBLIC, "icon.svg"); 13 | const PATH_FAVICON = path.join(PATH_PUBLIC, "favicon.svg"); 14 | 15 | const PATH_GEN_ICONS = path.join(PATH_PUBLIC, "icons/"); 16 | 17 | const BACKGROUND = 18 | "radial-gradient(circle, rgba(225,174,238,1) 0%, rgba(238,174,202,1) 100%)"; 19 | 20 | const GEN_ASSETS_OPTIONS = { 21 | scrape: true, 22 | log: false, 23 | type: "png", 24 | }; 25 | 26 | function relative(from, to) { 27 | return path 28 | .relative(from, to) 29 | .split(path.sep) 30 | .join("/"); 31 | } 32 | 33 | function resetDir(dir) { 34 | return new Promise((resolve, reject) => { 35 | rimraf(dir, err => { 36 | if (err) reject(err); 37 | else resolve(); 38 | }); 39 | }).then(() => fs.mkdir(dir, { recursive: true })); 40 | } 41 | 42 | function parseXml(xml) { 43 | const { childNodes } = parse5.parseFragment(xml); 44 | return childNodes 45 | .map(c => { 46 | if (c.nodeName === "#text") return null; 47 | return [ 48 | c.tagName, 49 | Object.assign( 50 | {}, 51 | ...c.attrs.map(({ name, value }) => ({ [name]: value })), 52 | ), 53 | ]; 54 | }) 55 | .filter(Boolean); 56 | } 57 | 58 | exports.generateAssets = async ({ splashScreen = true }) => { 59 | const [baseManifest] = await Promise.all([ 60 | fs.readFile(PATH_BASE_MANIFEST, "utf-8").then(text => { 61 | fs.writeFile(PATH_MANIFEST, text); 62 | return JSON.parse(text); 63 | }), 64 | resetDir(PATH_GEN_ICONS), 65 | ]); 66 | 67 | const { htmlMeta: htmlMetaIcon } = await generateImages( 68 | PATH_FAVICON, 69 | PATH_GEN_ICONS, 70 | { 71 | ...GEN_ASSETS_OPTIONS, 72 | opaque: false, 73 | favicon: true, 74 | iconOnly: true, 75 | log: false, 76 | type: "png", 77 | }, 78 | ); 79 | 80 | const { htmlMeta } = await generateImages(PATH_ICON, PATH_GEN_ICONS, { 81 | ...GEN_ASSETS_OPTIONS, 82 | background: BACKGROUND, 83 | padding: "0", 84 | manifest: PATH_MANIFEST, 85 | iconOnly: !splashScreen, 86 | }); 87 | 88 | const headStr = 89 | htmlMetaIcon.favicon + 90 | Object.keys(htmlMeta) 91 | .map(k => htmlMeta[k]) 92 | .join(""); 93 | 94 | const manifestDir = path.dirname(PATH_MANIFEST); 95 | 96 | const head = parseXml(headStr).map(([tag, attrs, ...args]) => { 97 | if (attrs.href) { 98 | attrs = { 99 | ...attrs, 100 | href: relative(manifestDir, attrs.href), 101 | }; 102 | } 103 | 104 | return [tag, attrs, ...args]; 105 | }); 106 | 107 | return { 108 | head, 109 | name: baseManifest.name, 110 | description: baseManifest.description, 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /docs/.vuepress/scripts/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs").promises; 4 | const { generateAssets } = require("./gen-assets"); 5 | const { SITE_META_FILE } = require("./constants"); 6 | 7 | const main = async () => { 8 | const res = await generateAssets({ splashScreen: false }); 9 | fs.writeFile(SITE_META_FILE, JSON.stringify(res)); 10 | }; 11 | 12 | main(); 13 | -------------------------------------------------------------------------------- /docs/.vuepress/scripts/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "GitPkg", 3 | "short_name": "GitPkg", 4 | "description": "Using sub folders of a repo as yarn/npm dependencies made easy", 5 | "background_color": "white", 6 | "categories": ["utilities", "dev"], 7 | "display": "standalone", 8 | "orientation": "portrait-primary", 9 | "scope": "./", 10 | "theme_color": "#F06292", 11 | "start_url": "./" 12 | } 13 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | pkg-outline() 2 | border-color transparent 3 | outline-style dashed 4 | outline-offset -3px 5 | outline-width 3px 6 | 7 | pkg-border(c) 8 | border-style solid 9 | border-width: w = 1px 10 | border-color c 11 | border-radius 0 12 | 13 | &.with-left 14 | border-left-width 0px 15 | 16 | &.with-right 17 | border-right-width 0px 18 | 19 | &+.with-left 20 | border-left-width 1px 21 | 22 | $trans = all ease 0.2s 0s, box-shadow ease-out 0.4s 0s 23 | 24 | * 25 | transition $trans 26 | 27 | pkg-focus() 28 | &:focus 29 | box-shadow: 0.5em 0.5em 1em #2c3e506e; 30 | pkg-outline() 31 | outline-color $primaryColor 32 | outline-style solid 33 | outline-width 1px 34 | outline-offset -1px 35 | 36 | pkg-hover() 37 | &:hover 38 | pkg-outline() 39 | box-shadow: 0.5em 0.5em 2em #2c3e506e; 40 | outline-color $accentColor 41 | 42 | .gitpkg-input 43 | c($color) 44 | pkg-outline() 45 | outline-color $color 46 | 47 | box-sizing border-box 48 | font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace 49 | padding 0 1em 50 | height $unitSize 51 | appearance none 52 | 53 | pkg-border($textColor) 54 | 55 | pkg-focus() 56 | pkg-hover() 57 | 58 | &.success 59 | c $successColor 60 | 61 | &.error 62 | c $errorColor 63 | 64 | &::selection 65 | background-color $codeBgColor 66 | color lighten($accentColor, 10) 67 | 68 | .gitpkg-button 69 | c($color) 70 | pkg-outline() 71 | outline-color $color 72 | 73 | background-color: bg = rgb(240, 240, 240) 74 | text-align center 75 | cursor pointer 76 | 77 | pkg-border($textColor) 78 | 79 | pkg-focus() 80 | 81 | pkg-hover() 82 | 83 | * 84 | transition $trans, color 0s 85 | 86 | &:active 87 | pkg-outline() 88 | c darken($accentColor, 25%) 89 | background-color darken(bg, 10%) 90 | 91 | &.icon 92 | width $unitSize 93 | height $unitSize 94 | display flex 95 | justify-content center 96 | align-items center 97 | 98 | &.success 99 | color $successColor 100 | c $successColor 101 | 102 | &.error 103 | color $errorColor 104 | c $errorColor 105 | 106 | @import select 107 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | // https://vuepress.vuejs.org/config/#palette-styl 2 | 3 | $unitSize = 2.4em 4 | 5 | // colors 6 | $primaryColor = #2196F3; 7 | $successColor = #42b983; 8 | $infoColor = #42A5F5; 9 | $errorColor = #C30000; 10 | $warningColor = darken(#FFE564, 35%); 11 | 12 | $accentColor = #F06292; 13 | // $textColor = #2c3e50; 14 | // $borderColor = #eaecef; 15 | // $codeBgColor = #282c34; 16 | // $arrowBgColor = #ccc; 17 | $badgeTipColor = $infoColor; 18 | $badgeWarningColor = $warningColor; 19 | $badgeErrorColor = $errorColor; 20 | // // layout 21 | // $navbarHeight = 3.6rem; 22 | // $sidebarWidth = 20rem; 23 | // $contentWidth = 740px; 24 | // $homePageWidth = 960px; 25 | // // responsive breakpoints 26 | // $MQNarrow = 959px; 27 | // $MQMobile = 719px; 28 | // $MQMobileNarrow = 419px; 29 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/select.styl: -------------------------------------------------------------------------------- 1 | .gitpkg-select.v-select 2 | padding 0 3 | position relative 4 | height $unitSize 5 | 6 | box-sizing border-box 7 | & * 8 | box-sizing border-box 9 | 10 | .vs__dropdown-toggle 11 | width 100% 12 | max-width 100% 13 | height 100% 14 | 15 | display flex 16 | justify-content space-between 17 | 18 | pkg-border($textColor) 19 | 20 | pkg-focus() 21 | pkg-hover() 22 | 23 | .vs__actions 24 | display flex 25 | 26 | > * 27 | @extend .gitpkg-button 28 | display flex 29 | justify-content center 30 | align-items center 31 | 32 | height 100% 33 | border none 34 | 35 | .vs__selected-options 36 | display flex 37 | 38 | padding 0 1em 39 | 40 | .vs__selected 41 | flex 0 1 auto 42 | display flex 43 | justify-content center 44 | align-items center 45 | 46 | .vs__search 47 | flex 0 1 auto 48 | display block 49 | width 0 50 | appearance none 51 | border none 52 | outline none 53 | 54 | &.vs--open .vs__open-indicator > * 55 | transform rotate(180deg) scale(1) 56 | 57 | .vs__dropdown-menu 58 | display block 59 | position absolute 60 | top 100% 61 | left 0 62 | z-index 1000 63 | margin 0 64 | padding 0 65 | width 100% 66 | overflow-y auto 67 | border-top-style none 68 | text-align left 69 | list-style none 70 | background white 71 | 72 | .vs__dropdown-option 73 | @extend .gitpkg-button 74 | border-width 0 75 | overflow visible 76 | &.vs__dropdown-option--selected 77 | background-color $accentColor 78 | color white 79 | &.vs__dropdown-option--highlight 80 | @extend .gitpkg-button:hover 81 | 82 | &.relaxed .vs__dropdown-toggle 83 | border 0 84 | 85 | &:not(:hover):not(:focus) 86 | box-shadow 0px 0 1px 0 black 87 | -------------------------------------------------------------------------------- /docs/.vuepress/yarn-svg-content.js: -------------------------------------------------------------------------------- 1 | export default ``; 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: /cover.svg 4 | footer: MIT Licensed | Copyright © 2020-present Equal Ma 5 | --- 6 | 7 |
8 | 9 | # How to use 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | You can also check out the detailed [guide](./guide/) 18 | 19 |
20 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: auto 3 | --- 4 | 5 | # GitPkg Guide 6 | 7 | ## Simplest API 8 | 9 | - Use a sub folder of a repo as dependency (master branch will be used) 10 | 11 | ``` 12 | https://gitpkg.now.sh// 13 | ``` 14 | 15 | - If you want to use another branch or commit instead 16 | 17 | ``` 18 | https://gitpkg.now.sh//? 19 | ``` 20 | 21 | ::: tip 22 | 23 | Usually, a commit-ish can be a `branch`, a `commit`, or a `tag`, etc. 24 | 25 | For more information, see: [git docs > commit-ish](https://git-scm.com/docs/gitglossary#Documentation/gitglossary.txt-aiddefcommit-ishacommit-ishalsocommittish) 26 | 27 | ::: 28 | 29 | - In fact, usage without sub folder is also available: 30 | 31 | `https://gitpkg.now.sh/` 32 | 33 | `https://gitpkg.now.sh/?` 34 | 35 | But `yarn add` and `npm install` support using github url directly: 36 | 37 | `` 38 | 39 | `#` 40 | 41 | Examples: 42 | 43 | ```shell 44 | yarn init -y 45 | 46 | # dep: repo=[EqualMa/gitpkg-hello] > sub folder=[packages/hello] 47 | yarn add https://gitpkg.now.sh/EqualMa/gitpkg-hello/packages/hello 48 | 49 | # dep: [EqualMa/gitpkg-hello] > [packages/core] # branch=[feat/md] 50 | yarn add https://gitpkg.now.sh/EqualMa/gitpkg-hello/packages/core?feat/md 51 | ``` 52 | 53 | ## More Formal API 54 | 55 | ``` 56 | https://gitpkg.now.sh/api/pkg?url=/ 57 | https://gitpkg.now.sh/api/pkg?url=/&commit= 58 | ``` 59 | 60 | Or if you want to make the file format clear: 61 | 62 | ``` 63 | https://gitpkg.now.sh/api/pkg.tgz?url=&commit= 64 | ``` 65 | 66 | ## Custom Scripts 67 | 68 | ### Why we need custom scripts 69 | 70 | Many github repositories contains source code only, which you can't use directly as a npm/yarn dependency. 71 | 72 | So this service provide the option to configure custom scripts 73 | 74 | ### How to use 75 | 76 | #### edit with the Web UI 77 | 78 | All you need is go to the [main page](/), 79 | click the `Add a custom script` button, 80 | edit the script name and content, 81 | then the install command will include the custom scripts. 82 | 83 | You can try the [example](#custom-script-example). 84 | 85 | #### (Advanced) setup the url by yourself 86 | 87 | If you don't want to use the UI, you can setup the url by your self 88 | 89 | - Simplest API 90 | 91 | ``` 92 | https://gitpkg.now.sh//?&scripts.= 93 | ``` 94 | 95 | - More Formal API 96 | 97 | ``` 98 | https://gitpkg.now.sh/pkg?url=/&scripts.= 99 | https://gitpkg.now.sh/pkg?url=/&commit=&scripts.= 100 | ``` 101 | 102 | ::: warning 103 | `` and `` must NOT contain special chars including `& ? =`. You can use `encodeURIComponent` to encode them before putting them in the query param. 104 | ::: 105 | 106 | ::: warning 107 | If you use windows, then using `yarn install ` or `npm install ` if `` contains `&` may lead to errors. 108 | 109 | In such cases, you have to manually add `"": ""` to the `dependency` or `devDependency` of `package.json`. 110 | ::: 111 | 112 | ##### replace, append to, or prepend to the original script 113 | 114 | If the original `package.json` contains the script with the same name, 115 | you can choose to whether to replace it. For example: 116 | 117 | The `package.json` is like: 118 | 119 | ```json 120 | { 121 | // ... 122 | "scripts": { 123 | "postinstall": "node original-install.js" 124 | } 125 | // ... 126 | } 127 | ``` 128 | 129 | - To **replace** the original, just use `scripts.postinstall=command-from-gitpkg`, 130 | then the generated `package.json` will be like: 131 | 132 | ```json 133 | { 134 | "scripts": { 135 | "postinstall": "command-from-gitpkg" 136 | } 137 | } 138 | ``` 139 | 140 | - To **append** to the original, add `&&` (encoded as `%26%26`) **before** your script content: `scripts.postinstall=%26%26command-from-gitpkg`. 141 | Then the generated `package.json` will be like: 142 | 143 | ```json 144 | { 145 | "scripts": { 146 | "postinstall": "node original-install.js && command-from-gitpkg" 147 | } 148 | } 149 | ``` 150 | 151 | - To **prepend** to the original, add `&&` **after** your script content: `scripts.postinstall=command-from-gitpkg%26%26`. 152 | Then the generated `package.json` will be like: 153 | 154 | ```json 155 | { 156 | "scripts": { 157 | "postinstall": "command-from-gitpkg && node original-install.js" 158 | } 159 | } 160 | ``` 161 | 162 | ### (Advanced) How this function is implemented 163 | 164 | GitPkg service process the tar file of the github repo in the form of stream, 165 | so that only a few memory is used. 166 | 167 | This means when a user (yarn or npm actually) requests `my-sub-dir` folder from repo `example-user/example-repo`, 168 | GitPkg service requests the tarball of whole repo `example-user/example-repo` from github, 169 | open a stream from the tarball response. 170 | 171 | Then the stream is parsed as an `tar entry stream`, 172 | and each entry is checked for whether it is in `my-sub-dir` folder. 173 | 174 | If yes, this entry is added to the stream which responses to the user. 175 | If not, this entry is ignored. 176 | 177 | To add the custom scripts, when an entry's path is `my-sub-dir/package.json`, 178 | this entry's file content will be loaded, 179 | and modified (the custom scripts are added to the `scripts` prop). 180 | Then the modified version is added to the stream which responses to the user. 181 | 182 | So when the user, or yarn and npm, receive the tarball, 183 | the tarball only contains files under `my-sub-dir`. 184 | And if custom scripts are specified, 185 | the `package.json` is modified to include the specified scripts. 186 | 187 | This is how GitPkg works. 188 | 189 | ## Examples 190 | 191 | ### Custom script example 192 | 193 | This example shows how to install [EqualMa/gitpkg-hello > packages/hello-ts](https://github.com/EqualMa/gitpkg-hello/tree/master/packages/hello-ts) as dependency. 194 | The sub folder of this repo only contains typescript source code so we need to use custom scripts: 195 | `scripts.postinstall=npm install --ignore-scripts && npm run build` 196 | 197 | ```shell 198 | mkdir hello-gitpkg 199 | cd hello-gitpkg 200 | npm init -y 201 | npm install 'https://gitpkg.now.sh/EqualMa/gitpkg-hello/packages/hello-ts?master&scripts.postinstall=npm%20install%20--ignore-scripts%20%26%26%20npm%20run%20build' 202 | ``` 203 | 204 | Then make a new file `test.js` 205 | 206 | ```js 207 | const { hello } = require("hello-ts"); 208 | console.log(hello("world")); 209 | ``` 210 | 211 | Running it should outputs `Hello world from TypeScript!` 212 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "independent", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true 5 | } 6 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": {}, 3 | "rewrites": [ 4 | { 5 | "source": "/([^/]+)(/)?", 6 | "destination": "/$1/index.html" 7 | }, 8 | { 9 | "source": "/api/pkg", 10 | "destination": "/api/pkg.ts" 11 | }, 12 | { 13 | "source": "/api/pkg.tgz", 14 | "destination": "/api/pkg.ts" 15 | }, 16 | { 17 | "source": "/((?:[^?]+/)+[^?]+)(\\?.+)?", 18 | "destination": "/api/pkg-from-url" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gitpkg/site", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "private": true, 6 | "workspaces": [ 7 | "docs/.vuepress", 8 | "tools/*", 9 | "packages/*" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/EqualMa/gitpkg.git" 14 | }, 15 | "scripts": { 16 | "lint": "eslint --cache --ext .js,.ts", 17 | "lint:fix": "eslint --cache --max-warnings 0 --fix", 18 | "test": "lerna run test", 19 | "now-build": "yarn run build && yarn run test", 20 | "build": "lerna run build" 21 | }, 22 | "devDependencies": { 23 | "@babel/core": "^7.8.7", 24 | "@babel/preset-env": "^7.8.7", 25 | "@babel/register": "^7.8.6", 26 | "@gitpkg/common": "*", 27 | "@now/node": "^1.4.1", 28 | "@types/jest": "^25.1.3", 29 | "@typescript-eslint/eslint-plugin": "^2.19.2", 30 | "@typescript-eslint/parser": "^2.19.2", 31 | "eslint": "^6.8.0", 32 | "eslint-config-prettier": "^6.10.0", 33 | "eslint-plugin-node": "^11.0.0", 34 | "eslint-plugin-prettier": "^3.1.2", 35 | "husky": "^4.2.3", 36 | "imagemin-lint-staged": "^0.4.0", 37 | "jest": "^25.1.0", 38 | "lerna": "^3.20.2", 39 | "lint-staged": "^10.0.7", 40 | "now": "^17.0.4", 41 | "prettier": "1.19.1", 42 | "rimraf": "^3.0.2", 43 | "ts-jest": "^25.2.1", 44 | "ts-node": "^8.6.2", 45 | "typescript": "^3.7.5" 46 | }, 47 | "dependencies": { 48 | "@gitpkg/core": "^1.0.0-alpha" 49 | }, 50 | "husky": { 51 | "hooks": { 52 | "pre-commit": "lint-staged && lerna run precommit" 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /packages/core/.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@gitpkg/common/eslint")(__dirname); 2 | -------------------------------------------------------------------------------- /packages/core/.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "!(.eslintrc).{js,ts}": "yarn run lint:fix" 2 | -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@gitpkg/common/babel")(); 2 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require("@gitpkg/common/jest")(); 2 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gitpkg/core", 3 | "version": "1.0.0-alpha", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.js", 6 | "license": "MIT", 7 | "scripts": { 8 | "test": "jest", 9 | "precommit": "lint-staged", 10 | "lint:fix": "eslint --cache --max-warnings 0 --fix", 11 | "build": "rimraf dist && tsc -p tsconfig.prod.json --outDir dist --module CommonJS --declaration --sourceMap --declarationMap" 12 | }, 13 | "dependencies": { 14 | "got": "^10.5.5", 15 | "path-to-regexp": "^6.1.0", 16 | "tar-transform": "^1.0.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/core/src/api/codeload-url.ts: -------------------------------------------------------------------------------- 1 | export function codeloadUrl (repo: string, commit: string) { 2 | if (!repo) throw Error('Missing repo') 3 | if (!commit) throw Error('Missing commit') 4 | 5 | return `https://codeload.github.com/${repo}/tar.gz/${commit}`; 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/api/download-git-pkg.ts: -------------------------------------------------------------------------------- 1 | import got from "got"; 2 | import * as tt from "tar-transform"; 3 | import { codeloadUrl } from "./codeload-url"; 4 | import { extractSubFolder } from "../tar/extract-sub-folder"; 5 | import { customScripts } from "../tar/custom-scripts"; 6 | import { prependPath } from "../tar/prepend-path"; 7 | import { PkgOptions } from "../parse-url-query"; 8 | 9 | export type PipelineItem = 10 | | NodeJS.ReadableStream 11 | | NodeJS.WritableStream 12 | | NodeJS.ReadWriteStream; 13 | 14 | export function pipelineToPkgTarEntries( 15 | pkgOpts: PkgOptions, 16 | ): NodeJS.ReadWriteStream[] { 17 | const { customScripts: cs, commitIshInfo: cii } = pkgOpts; 18 | 19 | return [ 20 | extractSubFolder(cii.subdir), 21 | cs && cs.length > 0 && customScripts(cs), 22 | prependPath("package/"), 23 | ].filter(Boolean) as NodeJS.ReadWriteStream[]; 24 | } 25 | 26 | export function pipelineToDownloadGitPkg(pkgOpts: PkgOptions): PipelineItem[] { 27 | const { commitIshInfo: cii } = pkgOpts; 28 | 29 | const tgzUrl = codeloadUrl(`${cii.user}/${cii.repo}`, cii.commit); 30 | 31 | return [ 32 | got.stream(tgzUrl), 33 | tt.extract({ gzip: true }), 34 | ...pipelineToPkgTarEntries(pkgOpts), 35 | tt.pack({ gzip: true }), 36 | ]; 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./download-git-pkg"; 2 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | export { getDefaultParser } from "./parse-url-query"; 3 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/default-parser.ts: -------------------------------------------------------------------------------- 1 | import { PkgOptionsParser } from "./parser"; 2 | import { getUrlAndCommitPlugin, customScriptsPlugin } from "./plugins"; 3 | 4 | const getParser = (parseFromUrl: boolean) => 5 | new PkgOptionsParser() 6 | .withPlugin(getUrlAndCommitPlugin(parseFromUrl)) 7 | .withPlugin(customScriptsPlugin); 8 | 9 | const parserFromUrl = getParser(true); 10 | const parserFromQuery = getParser(false); 11 | 12 | export const getDefaultParser = (parseFromUrl = false) => 13 | parseFromUrl ? parserFromUrl : parserFromQuery; 14 | 15 | export type PkgOptions = ReturnType< 16 | typeof getDefaultParser 17 | > extends PkgOptionsParser 18 | ? T 19 | : never; 20 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/error.ts: -------------------------------------------------------------------------------- 1 | export class UrlInvalidError extends Error { 2 | constructor(msg?: string) { 3 | super(msg); 4 | } 5 | } 6 | 7 | export class QueryParamsInvalidError extends Error { 8 | constructor(public readonly key: string | string[], msg?: string) { 9 | super(msg); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/get-value.ts: -------------------------------------------------------------------------------- 1 | import { PkgUrlAndCommitOptions } from "./plugins"; 2 | 3 | export function getValueOfQuery( 4 | query: import("@now/node").NowRequestQuery, 5 | key: string, 6 | options: PkgUrlAndCommitOptions, 7 | ): undefined | string | string[] { 8 | const commitFromUrl: string | undefined = options.parsedFromUrl 9 | ? options.commit 10 | : undefined; 11 | const v: undefined | string | string[] = query[key]; 12 | 13 | if (key === commitFromUrl) { 14 | if (typeof v === "string") { 15 | return v === commitFromUrl ? undefined : v; 16 | } else if (Array.isArray(v)) { 17 | if (v.length === 0) return undefined; 18 | if (v[0] === commitFromUrl) { 19 | if (v.length === 1) return undefined; 20 | else v.slice(1); 21 | } 22 | } 23 | } else return v; 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./parser"; 2 | export * from "./default-parser"; 3 | 4 | export * from "./error"; 5 | export * from "./get-value"; 6 | export * from "./plugins"; 7 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/parser.ts: -------------------------------------------------------------------------------- 1 | type Overwrite = { 2 | [K in keyof T | keyof U]: K extends keyof U 3 | ? U[K] 4 | : K extends keyof T 5 | ? T[K] 6 | : never; 7 | }; 8 | 9 | export type PkgOptionsParserPlugin = ( 10 | url: string, 11 | query: import("@now/node").NowRequestQuery, 12 | previousOptions: T, 13 | ) => U; 14 | 15 | export class PkgOptionsParser { 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | private plugins: PkgOptionsParserPlugin[] = []; 18 | 19 | public withPlugin( 20 | plugin: PkgOptionsParserPlugin, 21 | ): PkgOptionsParser> { 22 | const p = new PkgOptionsParser>(); 23 | p.plugins.push(...this.plugins, plugin); 24 | return p; 25 | } 26 | 27 | public parse( 28 | requestUrl: string, 29 | query: import("@now/node").NowRequestQuery, 30 | ): T { 31 | return this.plugins.reduce((options, plugin) => { 32 | return { 33 | ...(options as object), 34 | ...plugin(requestUrl, query, options), 35 | }; 36 | }, {}) as T; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/custom-scripts/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./plugin"; 2 | export * from "./parse-query"; 3 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/custom-scripts/parse-query.spec.ts: -------------------------------------------------------------------------------- 1 | import * as impl from "./parse-query"; 2 | 3 | test("check if query key is custom script", () => { 4 | expect(impl.queryKeyIsCustomScript("scripts.postinstall")).toBe(true); 5 | expect(impl.queryKeyIsCustomScript("scripts.")).toBe(true); 6 | 7 | expect(impl.queryKeyIsCustomScript("scripts")).toBe(false); 8 | }); 9 | 10 | test("parse script (string)", () => { 11 | expect(() => { 12 | impl.parseQueryAsCustomScript("foo", "yarn build"); 13 | }).toThrowError(); 14 | 15 | expect( 16 | impl.parseQueryAsCustomScript("scripts.postinstall", "yarn build"), 17 | ).toStrictEqual({ 18 | name: "postinstall", 19 | script: "yarn build", 20 | type: "replace", 21 | }); 22 | 23 | expect( 24 | impl.parseQueryAsCustomScript("scripts.postinstall", "&& yarn build"), 25 | ).toStrictEqual({ 26 | name: "postinstall", 27 | script: "yarn build", 28 | type: "append", 29 | }); 30 | 31 | expect( 32 | impl.parseQueryAsCustomScript("scripts.postinstall", "yarn build &&"), 33 | ).toStrictEqual({ 34 | name: "postinstall", 35 | script: "yarn build", 36 | type: "prepend", 37 | }); 38 | }); 39 | 40 | test("parse script (string array)", () => { 41 | expect( 42 | impl.parseQueryAsCustomScript("scripts.postinstall", [ 43 | "yarn install", 44 | "yarn build", 45 | ]), 46 | ).toStrictEqual({ 47 | name: "postinstall", 48 | script: "yarn install && yarn build", 49 | type: "replace", 50 | }); 51 | 52 | expect( 53 | impl.parseQueryAsCustomScript("scripts.postinstall", [ 54 | "&& yarn install", 55 | "yarn build", 56 | ]), 57 | ).toStrictEqual({ 58 | name: "postinstall", 59 | script: "yarn install && yarn build", 60 | type: "append", 61 | }); 62 | 63 | expect( 64 | impl.parseQueryAsCustomScript("scripts.postinstall", [ 65 | "yarn install", 66 | "yarn build &&", 67 | ]), 68 | ).toStrictEqual({ 69 | name: "postinstall", 70 | script: "yarn install && yarn build", 71 | type: "prepend", 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/custom-scripts/parse-query.ts: -------------------------------------------------------------------------------- 1 | export interface PkgCustomScript { 2 | name: string; 3 | script: string; 4 | type: "prepend" | "append" | "replace"; 5 | } 6 | 7 | const SCRIPTS_PREFIX = "scripts."; 8 | 9 | export function queryKeyIsCustomScript( 10 | key: string | symbol | number, 11 | ): key is string { 12 | return typeof key === "string" && key.startsWith(SCRIPTS_PREFIX); 13 | } 14 | 15 | function trimAndAnd(v: string) { 16 | return v 17 | .slice(v.startsWith("&&") ? 2 : 0, v.endsWith("&&") ? -2 : undefined) 18 | .trim(); 19 | } 20 | 21 | export function parseQueryAsCustomScript( 22 | key: string, 23 | value: string | string[], 24 | ): PkgCustomScript { 25 | if (!queryKeyIsCustomScript(key)) { 26 | throw new Error("query key is not valid as a custom script"); 27 | } 28 | 29 | let type: PkgCustomScript["type"] | undefined = undefined; 30 | let script: string; 31 | 32 | const name = key.slice(SCRIPTS_PREFIX.length); 33 | 34 | if (typeof value === "string") { 35 | const v = value.trim(); 36 | type = v.startsWith("&&") 37 | ? "append" 38 | : v.endsWith("&&") 39 | ? "prepend" 40 | : "replace"; 41 | script = trimAndAnd(v); 42 | } else { 43 | const s = []; 44 | for (const [i, val] of value.entries()) { 45 | const v = val.trim(); 46 | if (i === 0 && v.startsWith("&&")) { 47 | type = "append"; 48 | } else if (i === value.length - 1 && v.endsWith("&&")) { 49 | type = "prepend"; 50 | } 51 | 52 | s.push(trimAndAnd(v)); 53 | } 54 | script = s.join(" && "); 55 | } 56 | 57 | return { 58 | name, 59 | script, 60 | type: type === undefined ? "replace" : type, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/custom-scripts/plugin.ts: -------------------------------------------------------------------------------- 1 | import { PkgOptionsParserPlugin } from "../../parser"; 2 | import { 3 | PkgCustomScript, 4 | queryKeyIsCustomScript, 5 | parseQueryAsCustomScript, 6 | } from "./parse-query"; 7 | import { PkgUrlAndCommitOptions } from "../url-and-commit"; 8 | import { getValueOfQuery } from "../../get-value"; 9 | 10 | export interface PkgCustomScriptsOptions { 11 | customScripts: PkgCustomScript[]; 12 | } 13 | 14 | export const customScriptsPlugin: PkgOptionsParserPlugin< 15 | PkgUrlAndCommitOptions, 16 | PkgCustomScriptsOptions 17 | > = (requestUrl, query, previousOptions) => { 18 | return { 19 | customScripts: Reflect.ownKeys(query) 20 | .map(k => { 21 | if (queryKeyIsCustomScript(k)) { 22 | const v = getValueOfQuery(query, k, previousOptions); 23 | if (v) { 24 | return parseQueryAsCustomScript(k, v); 25 | } else return null; 26 | } else { 27 | return null; 28 | } 29 | }) 30 | .filter((Boolean as unknown) as (v: unknown) => v is PkgCustomScript), 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./url-and-commit"; 2 | export * from "./custom-scripts"; 3 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/url-and-commit/commit-ish.ts: -------------------------------------------------------------------------------- 1 | import { match } from "path-to-regexp"; 2 | import { PkgUrlAndCommitOptions } from "./plugin"; 3 | import { UrlInvalidError, QueryParamsInvalidError } from "../../error"; 4 | 5 | interface CommitIshInfoMatchResult { 6 | user: string; 7 | repo: string; 8 | subdirs?: string[]; 9 | } 10 | const matchCommitIshInfo = match( 11 | ":user/:repo/:subdirs*(/)?", 12 | ); 13 | 14 | export interface CommitIshInfo { 15 | user: string; 16 | repo: string; 17 | /** "" or a string which ends with "/" */ 18 | subdir: string; 19 | subdirs: string[] | undefined; 20 | commit: string; 21 | } 22 | 23 | const DEFAULT_COMMIT_ISH = "master"; 24 | 25 | export function parseCommitIshInfo( 26 | url: PkgUrlAndCommitOptions["url"], 27 | commit: PkgUrlAndCommitOptions["commit"], 28 | parsedFromUrl: PkgUrlAndCommitOptions["parsedFromUrl"], 29 | ): CommitIshInfo { 30 | const res = matchCommitIshInfo(url); 31 | 32 | if (res === false) { 33 | throw parsedFromUrl 34 | ? new UrlInvalidError() 35 | : new QueryParamsInvalidError("url"); 36 | } 37 | 38 | const { user, repo, subdirs } = res.params; 39 | return { 40 | user, 41 | repo, 42 | subdir: subdirs ? subdirs.join("/") + "/" : "", 43 | subdirs, 44 | commit: commit || DEFAULT_COMMIT_ISH, 45 | }; 46 | } 47 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/url-and-commit/from-query.ts: -------------------------------------------------------------------------------- 1 | import { PkgOptionsParserPlugin } from "../../parser"; 2 | import { QueryParamsInvalidError } from "../../error"; 3 | import { PkgUrlAndCommitOptions } from "./plugin"; 4 | import { parseCommitIshInfo } from "./commit-ish"; 5 | 6 | export const fromQuery: PkgOptionsParserPlugin< 7 | unknown, 8 | PkgUrlAndCommitOptions 9 | > = (requestUrl, query) => { 10 | const { url, commit } = query; 11 | if (typeof url !== "string") { 12 | throw new QueryParamsInvalidError("url"); 13 | } 14 | if (typeof commit !== "string" && typeof commit !== "undefined") { 15 | throw new QueryParamsInvalidError("commit"); 16 | } 17 | return { 18 | url, 19 | commit, 20 | parsedFromUrl: false, 21 | commitIshInfo: parseCommitIshInfo(url, commit, false), 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/url-and-commit/from-url.ts: -------------------------------------------------------------------------------- 1 | import { match } from "path-to-regexp"; 2 | import { PkgOptionsParserPlugin } from "../../parser"; 3 | import { PkgUrlAndCommitOptions } from "./plugin"; 4 | import { parseCommitIshInfo } from "./commit-ish"; 5 | import { QueryParamsInvalidError, UrlInvalidError } from "../../error"; 6 | 7 | const matchFromUrl = match( 8 | "/:url((?:[^?]+/)+[^?]+){\\?:commit([^?&=]+)}?(.+)?", 9 | ); 10 | 11 | interface MatchResult { 12 | url: string; 13 | commit?: string; 14 | } 15 | 16 | export const fromUrl: PkgOptionsParserPlugin< 17 | unknown, 18 | PkgUrlAndCommitOptions 19 | > = (requestUrl, query) => { 20 | const res = matchFromUrl(requestUrl); 21 | if (!res) { 22 | throw new UrlInvalidError(); 23 | } else { 24 | const { url: u, commit: c } = res.params; 25 | const url = decodeURIComponent(u); 26 | const commit = c && decodeURIComponent(c); 27 | 28 | if (query.commit) 29 | // url = /foo/bar?master&commit=master 30 | throw new QueryParamsInvalidError( 31 | "commit", 32 | `param commit is specified from both url(${url}) and query(commit=${commit})`, 33 | ); 34 | 35 | return { 36 | url, 37 | commit, 38 | parsedFromUrl: true, 39 | commitIshInfo: parseCommitIshInfo(url, commit, true), 40 | }; 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/url-and-commit/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./plugin"; 2 | -------------------------------------------------------------------------------- /packages/core/src/parse-url-query/plugins/url-and-commit/plugin.ts: -------------------------------------------------------------------------------- 1 | import { PkgOptionsParserPlugin } from "../../parser"; 2 | import { fromUrl } from "./from-url"; 3 | import { fromQuery } from "./from-query"; 4 | import { CommitIshInfo } from "./commit-ish"; 5 | 6 | export { CommitIshInfo } from "./commit-ish"; 7 | export interface PkgUrlAndCommitOptions { 8 | url: string; 9 | commit: undefined | string; 10 | parsedFromUrl: boolean; 11 | commitIshInfo: CommitIshInfo; 12 | } 13 | 14 | export const getUrlAndCommitPlugin = ( 15 | parseFromUrl = false, 16 | ): PkgOptionsParserPlugin => 17 | parseFromUrl ? fromUrl : fromQuery; 18 | -------------------------------------------------------------------------------- /packages/core/src/tar/custom-scripts.spec.ts: -------------------------------------------------------------------------------- 1 | import * as impl from "./custom-scripts"; 2 | import { PkgCustomScript } from "../parse-url-query"; 3 | import { TarEntry } from "tar-transform"; 4 | import { tarEntries, getEntries } from "../../test/util/tar-entry"; 5 | import { Readable, pipeline as _pl } from "stream"; 6 | import { promisify } from "util"; 7 | 8 | const pipeline = promisify(_pl); 9 | 10 | const ADD_TYPES: PkgCustomScript["type"][] = ["append", "prepend", "replace"]; 11 | 12 | type TestCase = [ 13 | Record, 14 | PkgCustomScript[], 15 | Record, 16 | ]; 17 | 18 | const testCases = (): TestCase[] => [ 19 | [{}, [], {}], 20 | ...ADD_TYPES.map(type => [ 21 | {}, 22 | [{ name: "build", script: "tsc", type }], 23 | { build: "tsc" }, 24 | ]), 25 | ...ADD_TYPES.map(type => { 26 | const res = { 27 | append: "tsc && echo 'success'", 28 | prepend: "echo 'success' && tsc", 29 | replace: "echo 'success'", 30 | }; 31 | return [ 32 | { build: "tsc" }, 33 | [{ name: "build", script: "echo 'success'", type }], 34 | { build: res[type] }, 35 | ]; 36 | }), 37 | ...ADD_TYPES.map(type => { 38 | const res = { 39 | append: "tsc && echo 'success'", 40 | prepend: "echo 'success' && tsc", 41 | replace: "echo 'success'", 42 | }; 43 | return [ 44 | { build: "tsc", test: "jest" }, 45 | [ 46 | { name: "build", script: "echo 'success'", type }, 47 | { name: "postinstall", script: "npm run build", type: "replace" }, 48 | ], 49 | { 50 | build: res[type], 51 | test: "jest", 52 | postinstall: "npm run build", 53 | }, 54 | ]; 55 | }), 56 | ]; 57 | 58 | test("add scripts to package.json", () => { 59 | for (const [scripts, add, res] of testCases()) { 60 | const pkg = { scripts }; 61 | impl.addScriptsToPkgJson(pkg, add); 62 | expect(pkg).toEqual({ scripts: res }); 63 | } 64 | }); 65 | 66 | function* tarEntriesWithPkgJson( 67 | insertIndex = 0, 68 | content: string, 69 | ...args: Parameters 70 | ): Generator { 71 | let inserted = false; 72 | 73 | const pkgJson: TarEntry = { 74 | headers: { name: "package.json" }, 75 | content, 76 | }; 77 | 78 | let i = 0; 79 | for (const e of tarEntries(...args)) { 80 | if (i === insertIndex) { 81 | inserted = true; 82 | yield pkgJson; 83 | } 84 | yield e; 85 | i++; 86 | } 87 | 88 | if (!inserted) yield pkgJson; 89 | } 90 | 91 | test("add scripts to tar entry stream", () => 92 | Promise.all( 93 | testCases() 94 | .map(([scripts, add, res]) => { 95 | return [0, 5, -1].map(insertIndex => { 96 | const r = Readable.from( 97 | tarEntriesWithPkgJson(insertIndex, JSON.stringify({ scripts }), { 98 | count: 10, 99 | }), 100 | ); 101 | const t = impl.customScripts(add); 102 | 103 | return [ 104 | expect(pipeline(r, t)).resolves.toBeUndefined(), 105 | expect(getEntries(t)).resolves.toEqual([ 106 | ...tarEntriesWithPkgJson( 107 | insertIndex, 108 | JSON.stringify({ scripts: res }, undefined, 2), 109 | { count: 10 }, 110 | ), 111 | ]), 112 | ]; 113 | }); 114 | }) 115 | .flat(2), 116 | )); 117 | -------------------------------------------------------------------------------- /packages/core/src/tar/custom-scripts.ts: -------------------------------------------------------------------------------- 1 | import { modifySingleFile } from "./modify-single-file"; 2 | import { PkgCustomScript } from "../parse-url-query"; 3 | 4 | export function addScriptsToPkgJson( 5 | pkgJson: Record, 6 | scripts: PkgCustomScript[], 7 | ) { 8 | const pkgScripts = (pkgJson.scripts || (pkgJson.scripts = {})) as Record< 9 | string, 10 | string 11 | >; 12 | 13 | for (const s of scripts) { 14 | const { type, name, script } = s; 15 | 16 | if (type === "replace") { 17 | pkgScripts[name] = script; 18 | } else if (type === "prepend") { 19 | const original = pkgScripts[name]; 20 | const str = original ? " && " + original : ""; 21 | pkgScripts[name] = script + str; 22 | } else if (type === "append") { 23 | const original = pkgScripts[name]; 24 | const str = original ? original + " && " : ""; 25 | pkgScripts[name] = str + script; 26 | } else throw new Error("prop type is invalid: " + type); 27 | } 28 | } 29 | 30 | export const customScripts = (scripts: PkgCustomScript[]) => 31 | modifySingleFile("package.json", async function(entry) { 32 | const pkgJson = JSON.parse(await this.util.stringContentOfTarEntry(entry)); 33 | addScriptsToPkgJson(pkgJson, scripts); 34 | return { content: JSON.stringify(pkgJson, undefined, 2) }; 35 | }); 36 | -------------------------------------------------------------------------------- /packages/core/src/tar/extract-sub-folder.spec.ts: -------------------------------------------------------------------------------- 1 | import { extractSubFolder } from "./extract-sub-folder"; 2 | import { TarEntry } from "tar-transform"; 3 | import { Readable, pipeline as _pipeline } from "stream"; 4 | import { tarEntries, getEntries } from "../../test/util/tar-entry"; 5 | 6 | import { promisify } from "util"; 7 | const pipeline = promisify(_pipeline); 8 | 9 | test("do not extract sub folder (only extract root folder)", () => { 10 | const read = Readable.from(tarEntries({ root: "root/" })); 11 | const t = extractSubFolder(""); 12 | return Promise.all([ 13 | pipeline(read, t), 14 | expect(getEntries(t)).resolves.toEqual([ 15 | ...tarEntries({ root: "" }), 16 | ]), 17 | ]); 18 | }); 19 | 20 | test("extract sub folder", () => { 21 | const sub = "dir1/"; 22 | 23 | const read = Readable.from(tarEntries({ root: "root/" })); 24 | const t = extractSubFolder(sub); 25 | return Promise.all([ 26 | pipeline(read, t), 27 | expect(getEntries(t)).resolves.toEqual( 28 | [...tarEntries({ root: "" })].filter(e => e.headers.name.startsWith(sub)), 29 | ), 30 | ]); 31 | }); 32 | 33 | test("throw error when there is multiple files or dirs at root", async () => { 34 | const read = Readable.from(tarEntries({ root: "" })); 35 | const t = extractSubFolder("dir1"); 36 | 37 | const done = expect(pipeline(read, t)).rejects.toThrowError(); 38 | t.read(); 39 | 40 | await done; 41 | }); 42 | -------------------------------------------------------------------------------- /packages/core/src/tar/extract-sub-folder.ts: -------------------------------------------------------------------------------- 1 | import * as tar from "tar-transform"; 2 | 3 | /** 4 | * 5 | * @param subFolder should be "" or end with "/" 6 | * @param prepend should be "" or end with "/" 7 | */ 8 | export const extractSubFolder = (subFolder: string, prepend = "") => 9 | tar.transform<{ root: undefined | string }>({ 10 | onEntry(entry): true { 11 | const ctx = this.ctx; 12 | const { 13 | headers, 14 | headers: { name }, 15 | } = entry; 16 | if (ctx.root === undefined) { 17 | if (entry.headers.type !== "directory") { 18 | throw new Error("invalid source file: first entry is not directory"); 19 | } 20 | ctx.root = name; 21 | return this.pass(entry); 22 | } else if (name.startsWith(ctx.root)) { 23 | if (headers.pax && headers.pax.path !== name) { 24 | throw new Error( 25 | "source file is not valid due to tarball pax header mismatch", 26 | ); 27 | } 28 | 29 | const dir = ctx.root + subFolder; 30 | if (name.startsWith(dir) && name.length > dir.length) { 31 | const newHeaders = this.util.headersWithNewName( 32 | headers, 33 | prepend + name.slice(dir.length), 34 | ); 35 | 36 | return this.push({ ...entry, headers: newHeaders }); 37 | } else return this.pass(entry); 38 | } else { 39 | throw new Error("invalid source file: multiple dirs in root"); 40 | } 41 | }, 42 | initCtx: { root: undefined }, 43 | }); 44 | -------------------------------------------------------------------------------- /packages/core/src/tar/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./custom-scripts"; 2 | export * from "./extract-sub-folder"; 3 | export * from "./modify-single-file"; 4 | export * from "./prepend-path"; 5 | -------------------------------------------------------------------------------- /packages/core/src/tar/modify-single-file.ts: -------------------------------------------------------------------------------- 1 | import { transform, TarEntry, TarEntryTransformer } from "tar-transform"; 2 | 3 | type MaybePromise = T | Promise; 4 | 5 | export const modifySingleFile = ( 6 | filePath: string, 7 | modify: ( 8 | this: TarEntryTransformer<{ matched: boolean }>, 9 | entry: TarEntry, 10 | ) => MaybePromise< 11 | { content: string } | { stream: import("stream").Readable } 12 | >, 13 | ) => 14 | transform({ 15 | async onEntry(entry): Promise { 16 | if (entry.headers.name === filePath) { 17 | if (this.ctx.matched) { 18 | throw new Error(`invalid state`); 19 | } 20 | this.ctx.matched = true; 21 | const data = await modify.call(this, entry); 22 | return this.push({ 23 | headers: entry.headers, 24 | ...data, 25 | }); 26 | } else { 27 | return this.push(entry); 28 | } 29 | }, 30 | onEnd() { 31 | if (!this.ctx.matched) { 32 | throw new Error(`file not found: ${filePath}`); 33 | } 34 | }, 35 | initCtx: { 36 | matched: false, 37 | }, 38 | }); 39 | -------------------------------------------------------------------------------- /packages/core/src/tar/prepend-path.spec.ts: -------------------------------------------------------------------------------- 1 | import * as impl from "./prepend-path"; 2 | import { tarEntries, getEntries } from "../../test/util/tar-entry"; 3 | import { Readable, pipeline as _pl } from "stream"; 4 | import { promisify } from "util"; 5 | 6 | const pipeline = promisify(_pl); 7 | 8 | test("prepend path", () => 9 | Promise.all( 10 | [undefined, "", "root", "root/"] 11 | .map(prepend => 12 | ["", "d2/"].map(root => { 13 | const r = Readable.from(tarEntries({ root })); 14 | const t = impl.prependPath(prepend); 15 | 16 | return [ 17 | expect(pipeline(r, t)).resolves.toBeUndefined(), 18 | expect(getEntries(t)).resolves.toEqual( 19 | [...tarEntries({ root })].map(e => ({ 20 | ...e, 21 | headers: { 22 | ...e.headers, 23 | name: (prepend || "") + e.headers.name, 24 | }, 25 | })), 26 | ), 27 | ]; 28 | }), 29 | ) 30 | .flat(2), 31 | )); 32 | -------------------------------------------------------------------------------- /packages/core/src/tar/prepend-path.ts: -------------------------------------------------------------------------------- 1 | import { transform } from "tar-transform"; 2 | 3 | /** 4 | * 5 | * @param prepend should be "" or end with "/". For example: `"package/"` 6 | */ 7 | export const prependPath = (prepend = "") => 8 | transform({ 9 | onEntry(entry) { 10 | this.push({ 11 | ...entry, 12 | headers: this.util.headersWithNewName( 13 | entry.headers, 14 | prepend + entry.headers.name, 15 | ), 16 | }); 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/core/test/util/tar-entry.ts: -------------------------------------------------------------------------------- 1 | import { TarEntry, isTarEntry } from "tar-transform"; 2 | import { Readable } from "stream"; 3 | 4 | export async function getEntries(r: Readable) { 5 | const entries: TarEntry[] = []; 6 | for await (const v of r) { 7 | if (isTarEntry(v)) { 8 | entries.push(v); 9 | } else { 10 | throw new Error("invalid tar entry"); 11 | } 12 | } 13 | return entries; 14 | } 15 | 16 | export function* tarEntries({ 17 | count = 10, 18 | depth = 3, 19 | root = "", 20 | } = {}): Generator { 21 | const dirs: Record = {}; 22 | if (root) { 23 | dirs[root] = true; 24 | yield { headers: { name: root, type: "directory" } }; 25 | } 26 | for (let i = 0; i < count; i++) { 27 | const dirName = 28 | root + [...new Array(i % depth).keys()].map(dir => `dir${dir}/`).join(""); 29 | 30 | if (dirName && !dirs[dirName]) { 31 | dirs[dirName] = true; 32 | yield { headers: { name: dirName, type: "directory" } }; 33 | } 34 | const fileName = dirName + `file${i}.data`; 35 | yield { headers: { name: fileName }, content: String(i) }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gitpkg/common/tsconfig", 3 | "include": ["src/**/*.ts", "test/**/*.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/tsconfig.prod.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gitpkg/common/tsconfig", 3 | "include": ["src/**/*.ts"], 4 | "exclude": ["node_modules", "**/*.spec.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tools/common/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./eslint/node")(); 2 | -------------------------------------------------------------------------------- /tools/common/.lintstagedrc.yml: -------------------------------------------------------------------------------- 1 | "!(.eslintrc).js": "yarn run lint:fix" 2 | -------------------------------------------------------------------------------- /tools/common/babel/index.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | presets: [ 3 | [ 4 | "@babel/env", 5 | { 6 | targets: { 7 | node: "6", 8 | }, 9 | // useBuiltIns: "usage", // TODO chore: core-js@3 10 | }, 11 | ], 12 | ], 13 | }); 14 | -------------------------------------------------------------------------------- /tools/common/eslint/index.js: -------------------------------------------------------------------------------- 1 | module.exports = rootDir => ({ 2 | root: true, 3 | overrides: [ 4 | { 5 | files: "**/*.ts", 6 | parser: "@typescript-eslint/parser", 7 | parserOptions: { 8 | tsconfigRootDir: rootDir, 9 | project: "./tsconfig.json", 10 | }, 11 | plugins: ["prettier", "@typescript-eslint"], 12 | extends: [ 13 | "eslint:recommended", 14 | "plugin:@typescript-eslint/eslint-recommended", 15 | "plugin:@typescript-eslint/recommended", 16 | "plugin:prettier/recommended", 17 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 18 | ], 19 | rules: { 20 | "@typescript-eslint/explicit-function-return-type": "off", 21 | }, 22 | }, 23 | { 24 | files: ["./*.js"], 25 | extends: [ 26 | "eslint:recommended", 27 | "plugin:prettier/recommended", 28 | "plugin:node/recommended-script", 29 | ], 30 | rules: { 31 | "node/no-extraneous-require": [ 32 | "error", 33 | { 34 | allowModules: ["@gitpkg/common"], 35 | }, 36 | ], 37 | }, 38 | }, 39 | ], 40 | }); 41 | -------------------------------------------------------------------------------- /tools/common/eslint/node.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | root: true, 3 | extends: [ 4 | "eslint:recommended", 5 | "plugin:prettier/recommended", 6 | "plugin:node/recommended", 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2020, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /tools/common/jest/index.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | preset: "ts-jest", 3 | testEnvironment: "node", 4 | globals: { 5 | "ts-jest": { 6 | babelConfig: true, 7 | }, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /tools/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gitpkg/common", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "engines": { 8 | "node": ">= 6" 9 | }, 10 | "scripts": { 11 | "lint:fix": "eslint --cache --max-warnings 0 --fix" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tools/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2019" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | // "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | 63 | /* Advanced Options */ 64 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 65 | }, 66 | "include": [] 67 | } 68 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gitpkg/common/tsconfig", 3 | "include": ["api/**/*.ts"] 4 | } 5 | --------------------------------------------------------------------------------