├── test
├── fixture
│ ├── stats
│ │ └── foo.md
│ ├── rename
│ │ └── a.txt
│ └── source
│ │ ├── should-filter.js
│ │ └── tmp.js
└── index.test.ts
├── .gitattributes
├── examples
└── scaffolding
│ ├── template
│ ├── lib
│ │ └── index.js
│ ├── LICENSE
│ └── test.js
│ ├── package.json
│ ├── README.md
│ └── start.js
├── .prettierrc
├── .gitignore
├── .editorconfig
├── jest.config.js
├── rollup.config.js
├── .babelrc
├── src
├── wares.ts
└── index.ts
├── circle.yml
├── LICENSE
├── package.json
├── README.md
└── tsconfig.json
/test/fixture/stats/foo.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/test/fixture/rename/a.txt:
--------------------------------------------------------------------------------
1 | a
2 |
--------------------------------------------------------------------------------
/test/fixture/source/should-filter.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/fixture/source/tmp.js:
--------------------------------------------------------------------------------
1 | const a = () => 'a'
2 |
--------------------------------------------------------------------------------
/examples/scaffolding/template/lib/index.js:
--------------------------------------------------------------------------------
1 | module.exports = 42
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/examples/scaffolding/template/LICENSE:
--------------------------------------------------------------------------------
1 | Sample License.
2 |
3 | {username}
4 |
--------------------------------------------------------------------------------
/examples/scaffolding/template/test.js:
--------------------------------------------------------------------------------
1 | // You should write unit test here!
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | *.log
4 | dist/
5 | output
6 | examples/*/yarn.lock
7 | typedoc
8 |
--------------------------------------------------------------------------------
/examples/scaffolding/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "inquirer": "^3.0.6",
4 | "majo": "^0.1.0",
5 | "pupa": "^1.0.0"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.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 |
11 | [*.md]
12 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/examples/scaffolding/README.md:
--------------------------------------------------------------------------------
1 | How does it work:
2 |
3 | - Use CLI prompt to retrive data from user
4 | - Render templates with prompt answers
5 | - Filter files with prompt answers
6 | - Write files
7 |
8 | Run:
9 |
10 | ```bash
11 | npm install
12 | node start
13 | ```
14 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | testEnvironment: 'node',
3 | transform: {
4 | '^.+\\.tsx?$': 'ts-jest'
5 | },
6 | testRegex: '(/__test__/.*|(\\.|/)(test|spec))\\.tsx?$',
7 | testPathIgnorePatterns: ['/node_modules/', '/dist/', '/types/'],
8 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
9 | }
10 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | input: 'src/index.ts',
3 | output: {
4 | dir: 'dist',
5 | format: 'cjs'
6 | },
7 | plugins: [
8 | require('rollup-plugin-typescript2')({
9 | tsconfigOverride: {
10 | compilerOptions: {
11 | module: 'esnext',
12 | declaration: true
13 | },
14 | include: ['src']
15 | }
16 | })
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | [
4 | "@babel/preset-env",
5 | {
6 | "modules": false,
7 | "targets": {
8 | "node": 6
9 | }
10 | }
11 | ]
12 | ],
13 | "env": {
14 | "test": {
15 | "presets": [
16 | [
17 | "@babel/preset-env",
18 | {
19 | "targets": {
20 | "node": "current"
21 | }
22 | }
23 | ]
24 | ]
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/wares.ts:
--------------------------------------------------------------------------------
1 | import { Majo, Middleware } from './'
2 |
3 | export default class Wares {
4 | middlewares: Middleware[]
5 |
6 | constructor() {
7 | this.middlewares = []
8 | }
9 |
10 | use(middleware: Middleware | Middleware[]) {
11 | this.middlewares = this.middlewares.concat(middleware)
12 |
13 | return this
14 | }
15 |
16 | run(context: Majo) {
17 | return this.middlewares.reduce((current, next) => {
18 | return current.then(() => Promise.resolve(next(context)))
19 | }, Promise.resolve())
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | build:
4 | docker:
5 | - image: circleci/node:10
6 | branches:
7 | ignore:
8 | - gh-pages # list of branches to ignore
9 | - /release\/.*/ # or ignore regexes
10 | steps:
11 | - checkout
12 | - restore_cache:
13 | key: dependency-cache-{{ checksum "yarn.lock" }}
14 | - run:
15 | name: install dependences
16 | command: yarn
17 | - save_cache:
18 | key: dependency-cache-{{ checksum "yarn.lock" }}
19 | paths:
20 | - ./node_modules
21 | - run:
22 | name: test
23 | command: yarn test
24 | - run:
25 | name: release
26 | command: npx semantic-release
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) egoist <0x142857@gmail.com> (https://egoistian.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/scaffolding/start.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const ask = require('inquirer')
3 | const pupa = require('pupa')
4 | const majo = require('majo')
5 |
6 | const stream = majo()
7 |
8 | stream
9 | .source('**', { baseDir: path.resolve('template') })
10 | .use(prompt)
11 | .use(template)
12 | .filter(filepath => {
13 | if (filepath === 'test.js' && !stream.meta.test) {
14 | return false
15 | }
16 | return true
17 | })
18 | .dest('./output')
19 | .then(() => {
20 | console.log(`> Done, checkout ./output directory`)
21 | })
22 | .catch(err => {
23 | console.error(err)
24 | process.exit(1)
25 | })
26 |
27 | function prompt(stream) {
28 | return ask.prompt([{
29 | name: 'username',
30 | message: `what's your name:`,
31 | validate: v => Boolean(v)
32 | }, {
33 | name: 'test',
34 | message: 'Do you want unit test:',
35 | type: 'confirm'
36 | }]).then(answers => {
37 | stream.meta = answers
38 | })
39 | }
40 |
41 | function template(stream) {
42 | for (const relative in stream.files) {
43 | const contents = stream.fileContents(relative)
44 | // Only does interpolation when `{}` appears
45 | if (/\{.+\}/.test(contents)) {
46 | stream.writeContents(relative, pupa(contents, stream.meta))
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "majo",
3 | "version": "0.0.0-semantic-release",
4 | "description": "A minimal module to manipulate files.",
5 | "repository": {
6 | "url": "egoist/majo",
7 | "type": "git"
8 | },
9 | "main": "dist/index.js",
10 | "types": "dist/index.d.ts",
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "test": "jest",
16 | "prepublishOnly": "npm run build",
17 | "build": "rollup -c",
18 | "format": "prettier *.{js,md,json,ts} --write",
19 | "typedoc": "typedoc src --out typedoc --theme minimal --mode file --excludeExternals --excludePrivate --excludeProtected"
20 | },
21 | "author": "egoist <0x142857@gmail.com>",
22 | "license": "MIT",
23 | "lint-staged": {
24 | "*.{js,md,json,ts}": [
25 | "prettier --write",
26 | "git add"
27 | ]
28 | },
29 | "husky": {
30 | "hooks": {
31 | "pre-commit": "lint-staged"
32 | }
33 | },
34 | "devDependencies": {
35 | "@types/fs-extra": "^8.0.1",
36 | "@types/jest": "^24.0.23",
37 | "@types/mkdirp": "^1.0.0",
38 | "@types/node": "^12.12.7",
39 | "@types/rimraf": "^3.0.0",
40 | "husky": "^3.0.9",
41 | "jest": "^24.9.0",
42 | "lint-staged": "^9.4.3",
43 | "prettier": "^1.19.1",
44 | "rollup": "^1.27.0",
45 | "rollup-plugin-typescript2": "^0.25.2",
46 | "ts-jest": "^24.1.0",
47 | "typedoc": "^0.15.1",
48 | "typescript": "^3.7.2"
49 | },
50 | "dependencies": {
51 | "fast-glob": "^3.1.0",
52 | "mkdirp": "^1.0.4",
53 | "rimraf": "^3.0.2"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/test/index.test.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import { majo, glob, remove } from '../src'
3 |
4 | test('main', async () => {
5 | const outputDir = path.join(__dirname, 'output/main')
6 | await remove(outputDir)
7 | const stream = await majo()
8 | .source('**', { baseDir: path.join(__dirname, 'fixture/source') })
9 | .dest('./output/main', { baseDir: __dirname })
10 | expect(
11 | await glob('**/*', { cwd: outputDir }).then(result => result.sort())
12 | ).toEqual(stream.fileList)
13 | })
14 |
15 | test('middleware', async () => {
16 | const stream = majo()
17 | .source('**', { baseDir: path.join(__dirname, 'fixture/source') })
18 | .use(({ files }) => {
19 | const contents = files['tmp.js'].contents.toString()
20 | files['tmp.js'].contents = Buffer.from(contents.replace(`'a'`, `'aaa'`))
21 | })
22 |
23 | await stream.process()
24 |
25 | expect(stream.fileContents('tmp.js')).toMatch(`const a = () => 'aaa'`)
26 | })
27 |
28 | test('filter', async () => {
29 | const stream = majo()
30 |
31 | stream
32 | .source('**', { baseDir: path.join(__dirname, 'fixture/source') })
33 | .filter(filepath => {
34 | return filepath !== 'should-filter.js'
35 | })
36 |
37 | await stream.process()
38 |
39 | expect(stream.fileList).toContain('tmp.js')
40 | expect(stream.fileList).not.toContain('should-filter.js')
41 | })
42 |
43 | test('stats', async () => {
44 | const stream = majo()
45 |
46 | stream.source('**/*.md', { baseDir: path.join(__dirname, 'fixture/stats') })
47 |
48 | await stream.process()
49 |
50 | expect(typeof stream.files['foo.md'].stats).toBe('object')
51 | })
52 |
53 | test('rename', async () => {
54 | const stream = majo()
55 |
56 | stream.source('**/*', { baseDir: path.join(__dirname, 'fixture/rename') })
57 |
58 | stream.use(ctx => {
59 | ctx.rename('a.txt', 'b/c.txt')
60 | })
61 |
62 | await stream.process()
63 |
64 | expect(stream.fileList).toEqual(['b/c.txt'])
65 | })
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # majo
2 |
3 |
4 |
5 |
6 |
7 | _Art by [でんでん COMIC1・こ 24b](https://www.pixiv.net/member.php?id=12192)_
8 |
9 | [](https://npmjs.com/package/majo) [](https://npmjs.com/package/majo) [](https://circleci.com/gh/egoist/majo/tree/master) [](https://github.com/egoist/donate)
10 |
11 | ## Introduction
12 |
13 | You can use _majo_ to manipulate files like a pro, with a simple API whose core is only ≈ 150 SLOC.
14 |
15 | ## Install
16 |
17 | ```bash
18 | yarn add majo
19 | ```
20 |
21 | ## Usage
22 |
23 | ```js
24 | const { majo } = require('majo')
25 |
26 | const stream = majo()
27 |
28 | // Given that you have js/app.js js/index.js
29 | stream
30 | .source('js/**')
31 | .use(ignoreSomeFiles)
32 | .dest('dist')
33 | .then(() => {
34 | // Now you got filtered files
35 | })
36 |
37 | function ignoreSomeFiles(stream) {
38 | for (const filename in stream.files) {
39 | const content = stream.fileContents(filename)
40 | // Remove it if content has specific string
41 | if (/some-string/.test(content)) {
42 | delete stream.files[filename]
43 | }
44 | }
45 | }
46 | ```
47 |
48 | ## Documentation
49 |
50 | https://majo.egoist.sh
51 |
52 | ## Used By
53 |
54 | - [SAO](https://github.com/egoist/sao): ⚔️ Futuristic scaffolding tool.
55 |
56 | ## Contributing
57 |
58 | 1. Fork it!
59 | 2. Create your feature branch: `git checkout -b my-new-feature`
60 | 3. Commit your changes: `git commit -am 'Add some feature'`
61 | 4. Push to the branch: `git push origin my-new-feature`
62 | 5. Submit a pull request :D
63 |
64 | ## Author
65 |
66 | **majo** © [egoist](https://github.com/egoist), Released under the [MIT](./LICENSE) License.
67 | Authored and maintained by egoist with help from contributors ([list](https://github.com/egoist/majo/contributors)).
68 |
69 | > [egoist.moe](https://egoist.moe) · GitHub [@egoist](https://github.com/egoist) · Twitter [@\_egoistlily](https://twitter.com/_egoistlily)
70 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | // "incremental": true, /* Enable incremental compilation */
5 | "target": "es2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
7 | "lib": [
8 | "esnext"
9 | ] /* Specify library files to be included in the compilation. */,
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
14 | // "sourceMap": true, /* Generates corresponding '.map' file. */
15 | // "outFile": "./", /* Concatenate and emit output to single file. */
16 | // "outDir": "./", /* Redirect output structure to the directory. */
17 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
18 | // "composite": true, /* Enable project compilation */
19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
20 | // "removeComments": true, /* Do not emit comments to output. */
21 | // "noEmit": true, /* Do not emit outputs. */
22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
25 |
26 | /* Strict Type-Checking Options */
27 | "strict": true /* Enable all strict type-checking options. */,
28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
29 | // "strictNullChecks": true, /* Enable strict null checks. */
30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
35 |
36 | /* Additional Checks */
37 | // "noUnusedLocals": true, /* Report errors on unused locals. */
38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */
39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
41 |
42 | /* Module Resolution Options */
43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
47 | // "typeRoots": [], /* List of folders to include type definitions from. */
48 | // "types": [], /* Type declaration files to be included in compilation. */
49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
52 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
53 |
54 | /* Source Map Options */
55 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
56 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
57 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
58 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
59 |
60 | /* Experimental Options */
61 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
62 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
63 |
64 | /* Advanced Options */
65 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
66 | },
67 | "exclude": ["node_modules"]
68 | }
69 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import path from 'path'
2 | import fs from 'fs'
3 | import { promisify } from 'util'
4 | import glob from 'fast-glob'
5 | import rimraf from 'rimraf'
6 | import ensureDir from 'mkdirp'
7 | import Wares from './wares'
8 |
9 | export type Middleware = (ctx: Majo) => Promise | void
10 |
11 | const readFile = promisify(fs.readFile)
12 | const writeFile = promisify(fs.writeFile)
13 | const remove = promisify(rimraf)
14 |
15 | export interface File {
16 | /** The absolute path of the file */
17 | path: string
18 | stats: fs.Stats
19 | contents: Buffer
20 | }
21 |
22 | export type FilterHandler = (relativePath: string, file: File) => boolean
23 |
24 | export type TransformHandler = (contents: string) => Promise | string
25 |
26 | export type OnWrite = (relativePath: string, outputPath: string) => void
27 |
28 | export interface SourceOptions {
29 | /**
30 | * The base directory to search files from
31 | * @default `process.cwd()`
32 | */
33 | baseDir?: string
34 | /**
35 | * Whether to include dot files
36 | * @default `true`
37 | */
38 | dotFiles?: boolean
39 | /** This function is called when a file is written */
40 | onWrite?: OnWrite
41 | }
42 |
43 | export interface DestOptions {
44 | /**
45 | * The base directory to write files to
46 | * @default `process.cwd()`
47 | */
48 | baseDir?: string
49 | /**
50 | * Whether to clean output directory before writing files
51 | * @default `false`
52 | */
53 | clean?: boolean
54 | }
55 |
56 | export class Majo {
57 | middlewares: Middleware[]
58 | /**
59 | * An object you can use across middleware to share states
60 | */
61 | meta: {
62 | [k: string]: any
63 | }
64 | /**
65 | * Base directory
66 | * You normally set this by calling `.source`
67 | */
68 | baseDir?: string
69 | sourcePatterns?: string[]
70 | dotFiles?: boolean
71 | files: {
72 | [filename: string]: File
73 | }
74 | onWrite?: OnWrite
75 |
76 | constructor() {
77 | this.middlewares = []
78 | this.meta = {}
79 | this.files = {}
80 | }
81 |
82 | /**
83 | * Find files from specific directory
84 | * @param source Glob patterns
85 | * @param opts
86 | * @param opts.baseDir The base directory to find files
87 | * @param opts.dotFiles Including dot files
88 | */
89 | source(patterns: string | string[], options: SourceOptions = {}) {
90 | const { baseDir = '.', dotFiles = true, onWrite } = options
91 | this.baseDir = path.resolve(baseDir)
92 | this.sourcePatterns = Array.isArray(patterns) ? patterns : [patterns]
93 | this.dotFiles = dotFiles
94 | this.onWrite = onWrite
95 | return this
96 | }
97 |
98 | /**
99 | * Use a middleware
100 | */
101 | use(middleware: Middleware) {
102 | this.middlewares.push(middleware)
103 | return this
104 | }
105 |
106 | /**
107 | * Process middlewares against files
108 | */
109 | async process() {
110 | if (!this.sourcePatterns || !this.baseDir) {
111 | throw new Error(`[majo] You need to call .source first`)
112 | }
113 |
114 | const allEntries = await glob(this.sourcePatterns, {
115 | cwd: this.baseDir,
116 | dot: this.dotFiles,
117 | stats: true
118 | })
119 |
120 | await Promise.all(
121 | allEntries.map(entry => {
122 | const absolutePath = path.resolve(this.baseDir as string, entry.path)
123 | return readFile(absolutePath).then(contents => {
124 | const file = {
125 | contents,
126 | stats: entry.stats as fs.Stats,
127 | path: absolutePath
128 | }
129 | // Use relative path as key
130 | this.files[entry.path] = file
131 | })
132 | })
133 | )
134 |
135 | await new Wares().use(this.middlewares).run(this)
136 |
137 | return this
138 | }
139 |
140 | /**
141 | * Filter files
142 | * @param fn Filter handler
143 | */
144 | filter(fn: FilterHandler) {
145 | return this.use(context => {
146 | for (const relativePath in context.files) {
147 | if (!fn(relativePath, context.files[relativePath])) {
148 | delete context.files[relativePath]
149 | }
150 | }
151 | })
152 | }
153 |
154 | /**
155 | * Transform file at given path
156 | * @param relativePath Relative path
157 | * @param fn Transform handler
158 | */
159 | async transform(relativePath: string, fn: TransformHandler) {
160 | const contents = this.files[relativePath].contents.toString()
161 | const newContents = await fn(contents)
162 | this.files[relativePath].contents = Buffer.from(newContents)
163 | }
164 |
165 | /**
166 | * Run middlewares and write processed files to disk
167 | * @param dest Target directory
168 | * @param opts
169 | * @param opts.baseDir Base directory to resolve target directory
170 | * @param opts.clean Clean directory before writing
171 | */
172 | async dest(dest: string, options: DestOptions = {}) {
173 | const { baseDir = '.', clean = false } = options
174 | const destPath = path.resolve(baseDir, dest)
175 | await this.process()
176 |
177 | if (clean) {
178 | await remove(destPath)
179 | }
180 |
181 | await Promise.all(
182 | Object.keys(this.files).map(filename => {
183 | const { contents } = this.files[filename]
184 | const target = path.join(destPath, filename)
185 | if (this.onWrite) {
186 | this.onWrite(filename, target)
187 | }
188 | return ensureDir(path.dirname(target)).then(() =>
189 | writeFile(target, contents)
190 | )
191 | })
192 | )
193 |
194 | return this
195 | }
196 |
197 | /**
198 | * Get file contents as a UTF-8 string
199 | * @param relativePath Relative path
200 | */
201 | fileContents(relativePath: string): string {
202 | return this.file(relativePath).contents.toString()
203 | }
204 |
205 | /**
206 | * Write contents to specific file
207 | * @param relativePath Relative path
208 | * @param string File content as a UTF-8 string
209 | */
210 | writeContents(relativePath: string, contents: string) {
211 | this.files[relativePath].contents = Buffer.from(contents)
212 | return this
213 | }
214 |
215 | /**
216 | * Get the fs.Stats object of specified file
217 | * @para relativePath Relative path
218 | */
219 | fileStats(relativePath: string): fs.Stats {
220 | return this.file(relativePath).stats
221 | }
222 |
223 | /**
224 | * Get a file by relativePath path
225 | * @param relativePath Relative path
226 | */
227 | file(relativePath: string): File {
228 | return this.files[relativePath]
229 | }
230 |
231 | /**
232 | * Delete a file
233 | * @param relativePath Relative path
234 | */
235 | deleteFile(relativePath: string) {
236 | delete this.files[relativePath]
237 | return this
238 | }
239 |
240 | /**
241 | * Create a new file
242 | * @param relativePath Relative path
243 | * @param file
244 | */
245 | createFile(relativePath: string, file: File) {
246 | this.files[relativePath] = file
247 | return this
248 | }
249 |
250 | /**
251 | * Get an array of sorted file paths
252 | */
253 | get fileList(): string[] {
254 | return Object.keys(this.files).sort()
255 | }
256 |
257 | rename(fromPath: string, toPath: string) {
258 | if (!this.baseDir) {
259 | return this
260 | }
261 | const file = this.files[fromPath]
262 | this.createFile(toPath, {
263 | path: path.resolve(this.baseDir, toPath),
264 | stats: file.stats,
265 | contents: file.contents
266 | })
267 | this.deleteFile(fromPath)
268 | return this
269 | }
270 | }
271 |
272 | const majo = () => new Majo()
273 |
274 | export { majo, remove, glob, ensureDir }
275 |
276 | /**
277 | * Ensure directory exists before writing file
278 | */
279 | export const outputFile = (
280 | filepath: string,
281 | data: any,
282 | options?: fs.WriteFileOptions
283 | ) =>
284 | ensureDir(path.dirname(filepath)).then(() =>
285 | writeFile(filepath, data, options)
286 | )
287 |
--------------------------------------------------------------------------------