Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(project-files): project files tool #39

Merged
merged 1 commit into from
Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
strict-peer-dependencies=false
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"@trigen/browserslist-config": "workspace:*",
"@trigen/eslint-config": "workspace:*",
"@trigen/lint-package-json": "workspace:*",
"@trigen/project-files": "workspace:*",
"@trigen/scripts": "workspace:*",
"@types/node": "^17.0.8",
"clean-publish": "^4.0.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/project-files/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": [
"@trigen/eslint-config/esm",
"@trigen/eslint-config/jest"
]
}
41 changes: 41 additions & 0 deletions packages/project-files/files.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# GitHub
.github/ISSUE_TEMPLATE/config.yml
.github/ISSUE_TEMPLATE/bug-report.yml
.github/ISSUE_TEMPLATE/feature-request.yml
.github/ISSUE_TEMPLATE/question.yml
# GitHub Actions
.github/workflows/ci.yml
.github/workflows/commit.yml
.github/workflows/checks.yml
.github/workflows/release.yml
.github/workflows/update-snapshots.yml
.github/workflows/website.yml
# CI
.github/renovate.json
.clean-publish
.eslintrc.json
test/.eslintrc.json
.size-limit.json
.npmpackagejsonlintrc.json
jest.config.json
# TypeScript
test/tsconfig.json
tsconfig.json
# Build
env.d.ts
.browserslistrc
rollup.config.js
# Monorepo
lerna.json
pnpm-workspace.yaml
# Git Flow
.gitignore
.nano-staged.json
.simple-git-hooks.json
.commitlintrc.json
.czrc
# Basics
.npmrc
.editorconfig
LICENSE
README.md
38 changes: 38 additions & 0 deletions packages/project-files/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@trigen/project-files",
"type": "module",
"version": "8.0.0-alpha.9",
"description": "CLI tool for download snippets files.",
"author": "dangreen",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/TrigenSoftware/scripts.git",
"directory": "packages/project-files"
},
"bugs": {
"url": "https://github.com/TrigenSoftware/scripts/issues"
},
"keywords": [
"files",
"snippets"
],
"engines": {
"node": ">=14"
},
"bin": "./src/bin.js",
"exports": "./src/index.js",
"publishConfig": {
"access": "public",
"directory": "package"
},
"scripts": {
"prepublishOnly": "del ./package && clean-publish",
"postpublish": "del ./package"
},
"dependencies": {
"@octokit/rest": "^19.0.4",
"inquirer": "^8.2.0"
},
"readme": ""
}
7 changes: 7 additions & 0 deletions packages/project-files/sources.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
TrigenSoftware/scripts
TrigenSoftware/simple-github-release
TrigenSoftware/Argue
canvg/canvg
reactchartjs/react-chartjs-2
chartist-js/chartist
TrigenSoftware/flexis-srcset
129 changes: 129 additions & 0 deletions packages/project-files/src/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { dirname, join } from 'path'
import { Octokit } from '@octokit/rest'
import { writeFile } from './fs.js'

/**
* @typedef {import('./parse.js').SourceItem} Repo
*/

const OK = 200
const { GITHUB_TOKEN } = process.env
const client = new Octokit({
auth: GITHUB_TOKEN ? `token ${GITHUB_TOKEN}` : ''
})

/**
* @typedef RepoFile
* @property {string} path - File path.
* @property {string} name - File name.
* @property {string} sha - The SHA of the file.
* @property {string} download_url - Download url.
* @property {string} html_url - Html url.
*/

/**
* Get files list from repository.
* @param {Repo} repo
* @param {string} path
* @returns {Promise<RepoFile[]>} File from the repository.
*/
export async function getDirectory(repo, path) {
let status
let data

try {
({
status,
data
} = await client.request('GET /repos/{owner}/{repo}/contents/{path}', {
owner: repo.owner,
repo: repo.repo,
path: path === '.' ? '' : path
}))
} catch (err) {
return []
}

if (status !== OK || !data) {
return []
}

return data
}

/**
* Get files filtered by sha from repository.
* @param {Repo[]} repos
* @param {string[]} paths
* @returns {Promise<Map<string, RepoFile[]>>} Files from the repository.
*/
export async function getFiles(repos, paths) {
const dirs = new Set()
const tasks = []
const shas = []
const filesMap = new Map()
let task
let dir
let file
let sha
let files

paths.forEach((path) => {
dirs.add(dirname(path))
})

repos.forEach((repo) => {
dirs.forEach((dir) => {
tasks.push(getDirectory(repo, dir))
})
})

for (task of tasks) {
dir = await task

for (file of dir) {
if (paths.includes(file.path)) {
sha = file.sha + file.path

if (!shas.includes(sha)) {
shas.push(sha)

files = filesMap.get(file.path)

if (!files) {
files = []
filesMap.set(file.path, files)
}

files.push(file)
}
}
}
}

return filesMap
}

/**
* Download file.
* @param {RepoFile} file
* @param {boolean | string} write
* @returns {Promise<string>} File contents.
*/
export async function downloadFile(file, write) {
const { data } = await client.request(`GET ${file.download_url}`)

if (write) {
await writeFile(
join(
write === true
? '.'
: write,
file.path
),
data
)
}

return data
}
39 changes: 39 additions & 0 deletions packages/project-files/src/bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env node
import { fileURLToPath } from 'url'
import { dirname, join } from 'path'
import {
readFile,
writeFile,
parseFilesList,
parseSourcesList,
getFiles,
downloadFile,
askFiles,
askSource
} from './index.js'

const { PROJECT_FILES_DIR } = process.env
const cwd = PROJECT_FILES_DIR ? join(process.cwd(), PROJECT_FILES_DIR) : process.cwd()
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..')
const parseFilesListTask = parseFilesList(readFile(join(pkgRoot, 'files.txt'), 'utf8'))
const parseSourcesListTask = parseSourcesList(readFile(join(pkgRoot, 'sources.txt'), 'utf8'))
const [filesList, sourcesList] = await Promise.all([parseFilesListTask, parseSourcesListTask])
const selectedFiles = await askFiles(filesList)
const files = await getFiles(sourcesList, selectedFiles)
let file
let fileSources
const tasks = []

for (file of selectedFiles) {
fileSources = files.get(file)

if (!fileSources) {
tasks.push(writeFile(join(cwd, file), ''))
} else if (fileSources.length === 1) {
tasks.push(downloadFile(fileSources[0], cwd))
} else {
tasks.push(downloadFile(await askSource(fileSources), cwd))
}
}

await Promise.all(tasks)
26 changes: 26 additions & 0 deletions packages/project-files/src/fs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { dirname } from 'path'
import { promises } from 'fs'

const { readFile, writeFile: wf, mkdir } = promises

export { readFile }

/**
* Write file.
* @param {string} path
* @param {string} contents
* @returns {Promise<void>}
*/
export async function writeFile(path, contents) {
const dir = dirname(path)

try {
await mkdir(dir, {
recursive: true
})
} catch (err) {
/* Ignore error */
}

return wf(path, contents)
}
4 changes: 4 additions & 0 deletions packages/project-files/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './parse.js'
export * from './api.js'
export * from './ui.js'
export * from './fs.js'
59 changes: 59 additions & 0 deletions packages/project-files/src/parse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* @typedef FileItem
* @property {string} type - File type description
* @property {string} path - File path
*/

/**
* Parse files list file.
* @param {string | Promise<string>} contents
* @returns {Promise<FileItem[]>} Parsed files info.
*/
export async function parseFilesList(contents) {
const text = await contents
const files = []
let type

text.split('\n').forEach((line) => {
if (line[0] === '#') {
type = line.slice(2)
} else if (line) {
files.push({
type,
path: line
})
}
})

return files
}

/**
* @typedef SourceItem
* @property {string} owner - Source repository owner
* @property {string} repo - Source repository name
*/

/**
* Parse sources list file.
* @param {string | Promise<string>} contents
* @returns {Promise<SourceItem[]>} Parsed sources info.
*/
export async function parseSourcesList(contents) {
const text = await contents
const sources = []
let owner
let repo

text.split('\n').forEach((line) => {
if (line) {
[owner, repo] = line.split('/')
sources.push({
owner,
repo
})
}
})

return sources
}