NPM - line-controller 패키지 배포

NPM

Posted by kwon on 2020-08-29

npm 패키지 제작기

블로그를 좀 꾸며 보려다 보니 이미 작성된 많은 글들에 카테고리별로 공통적인 line을 추가해야 할 일이 생겼다. 하나의 카테고리에 몇 가지 안되는 글이 있는 것은 괜찮았지만 100개 가까이 되는 카테고리인 경우에는 언제 하나하나 다 넣나 막막했다. 그래서 그냥 자동으로 원하는 line에 원하는 텍스트를 넣는 간단한 프로그램을 만들어 보았다.

만들어 본 김에 npm에 배포를 해보고 싶어서 몇 가지 기능을 좀 더 추가하여 배포해 보았다.

  • 초기 작성본
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const fs = require("fs")

const WRITE_DIR = './writeDirTest\\';
const TARGET_WORD = 'algorithm3';
const NEWLINE = '\r\n';

const UTF_8 = 'utf8';
const TARGET_LINE_NUM = 8 - 1;
const INSERT_LINE = `tags:`;
const INSERT_LINE2 = `\t- Algorithm`;

const readFileList = (dirPath) => {
return fs.readdirSync(dirPath, (err, fileList) => fileList);
}

const filterFileList = (fileList, targetWord) => {
return fileList.filter(fileName => {
return fileName.substr(0, targetWord.length) === targetWord
});
}

const readFileContents = (fileName) => {
return fs.readFileSync(WRITE_DIR + fileName, UTF_8, (err, contents) =>{});
}

const insert = (fileContent, targetLine) => {
let contentArr = fileContent.split(NEWLINE);
contentArr.splice(targetLine, 0, INSERT_LINE, INSERT_LINE2);
return contentArr;
}

const insertLines = (targetDir, targetWord, targetLine) => {
const fileList = readFileList(targetDir);
const filteredList = filterFileList(fileList, targetWord);

filteredList.forEach(fileName => {
const fileContents = readFileContents(fileName);
const inserted = insert(fileContents, targetLine);
const result = inserted.join(NEWLINE);

fs.writeFile(targetDir + fileName, result, () => {})

})
}

insertLines(WRITE_DIR, TARGET_WORD, TARGET_LINE_NUM);

위의 상태는 원하는 폴더에서 파일명이 TARGET_WORD 로 시작하는 파일의 TARGET_LINE_NUM 번째 라인에 원하는 텍스트를 추가하도록 구현했다. 이제 조금 더 확장성 있는 형태로 바꾸어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
const fs = require("fs");

const WRITE_DIR = './writeDirTest\\';
const TARGET_WORD = 'algorithm3';
const NEWLINE = '\r\n';

const UTF_8 = 'utf8';
const TARGET_LINE_NUM = 7;
const INSERT_LINE = `tags:`;
const INSERT_LINE2 = `\t- Algorithm`;

const readFileList = (dirPath) => {
return fs.readdirSync(dirPath, (err, fileList) => {
if (err) console.log(err);
});
}

const filterFileList = (fileList, targetWord) => {
return fileList.filter(fileName => {
return fileName.substr(0, targetWord.length) === targetWord
});
}

const readFileContents = (fileName) => {
return fs.readFileSync(WRITE_DIR + fileName, UTF_8, (err, contents) => {
if (err) console.log(err);
});
}

const insert = (fileContent, targetLine, lines) => {
let contentArr = fileContent.split(NEWLINE);
contentArr.splice(targetLine - 1, 0, ...lines);
return contentArr;
}

const insertLine = (targetDir, targetWord, targetLine, ...lines) => {
const fileList = readFileList(targetDir);
const filteredList = filterFileList(fileList, targetWord);

filteredList.forEach(fileName => {
const fileContents = readFileContents(fileName);
const inserted = insert(fileContents, targetLine, lines);
const result = inserted.join(NEWLINE);

fs.writeFile(targetDir + fileName, result, () => { })
})
}

insertLine(WRITE_DIR, TARGET_WORD, TARGET_LINE_NUM, INSERT_LINE, INSERT_LINE2);

rest parameter 를 활용해서 원하는 만큼 인자를 받아서 받은 인자를 모두 삽입할 수 있도록 구현했다.

삽입 기능만 있으면 심심하니까 삭제, 수정, 조회 기능을 추가하고 os 별 호환이 가능하도록 개행문자를 os모듈에서 받아 오도록 하여 사용했다. 또한 디렉토리 내에서 파일명이 특정 문자열로 시작하는 파일에 적용되도록 구현했던 것을 파일명에 특정 문자열이 포함되기만 하면 되도록 수정하였다.

  • 최종 배포 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
const fs = require('fs');
const os = require('os')
const NEWLINE = os.EOL;
const UTF_8 = 'utf8';

/**
* @description 원하는 디렉토리 내부의 파일 목록을 읽어온다.
* @param {String} dirPath 읽어올 디렉토리 경로
* @return {Array} 파일 목록을 배열로 리턴
*/
const readFileList = (dirPath) => {
return fs.readdirSync(dirPath, (err, fileList) => {
if (err) console.log('Error :', err);
});
}

/**
* @description 파일 목록에서 파일명에 특정 문자열이 포함된 파일들의 목록을 반환한다.
* @param {Array} fileList 전체 파일 목록
* @param {String} targetWord 특정 문자열
* @return {Array} targetWord가 파일명에 포함된 파일 목록 리턴
*/
const filterFileList = (fileList, targetWord) => {
return fileList.filter(fileName => fileName.indexOf(targetWord) > -1);
}

/**
* @description 파일 내용을 동기적으로 읽어오는 함수
* @param {String} fileName 파일명
* @param {String} targetDir 디렉토리 경로
* @return {String} 읽어온 파일 내용
*/
const readFileContents = (fileName, targetDir = '') => {
return fs.readFileSync(targetDir + fileName, UTF_8, (err, contents) => {
if (err) console.log('Error :', err);
});
}

/**
* @description 특정 line에 lines을 삽입한다.
* @param {String} fileContent 기존 파일 내용
* @param {Number} targetLine 삽입할 line number
* @param {Array} lines 삽입될 line들 목록
* @return {String} 삽입된 결과 문자열을 리턴
*/
const insert = (fileContent, targetLine, lines) => {
let contentArr = fileContent.split(NEWLINE);
contentArr.splice(targetLine - 1, 0, ...lines);
return contentArr.join(NEWLINE);
}

/**
* @description 특정 line에 lines을 삽입한다.
* @param {String} fileContent 기존 파일 내용
* @param {Number} startLine 제거할 시작 line number
* @param {Number} deleteCount 제거될 line 개수
* @return {String} line이 제거된 결과 문자열을 리턴
*/
const remove = (fileContent, startLine, deleteCount) => {
let contentArr = fileContent.split(NEWLINE);
contentArr.splice(startLine - 1, deleteCount);
return contentArr.join(NEWLINE);
}

/**
* @description 특정 line을 수정한다.
* @param {String} fileContent 기존 파일 내용
* @param {Number} targetLine 수정할 line number
* @param {String} text 수정할 내용
* @return {String} 수정된 결과 문자열을 리턴
*/
const update = (fileContent, targetLine, text) => {
let contentArr = fileContent.split(NEWLINE);
contentArr.splice(targetLine - 1, 1, text);
return contentArr.join(NEWLINE);
}

/**
* @description 특정 lines을 조회한다.
* @param {String} fileContent 기존 파일 내용
* @param {Number} targetLine 조회할 line number
* @param {Number} selectCount 조회할 line 개수
* @return {String} 조회된 결과 문자열을 리턴
*/
const select = (fileContent, targetLine, selectCount) => {
let contentArr = fileContent.split(NEWLINE);
return contentArr.filter((_, idx) =>
idx >= targetLine - 1 && idx < targetLine - 1 + selectCount).join(NEWLINE);
}

/**
* @description targetDir에 있는 파일들 중 파일명에 targetWord가 포함되는 파일에 원하는 line들을 삽입한다.
* @param {String} targetDir 원하는 디렉토리의 경로 ex) 'files/'
* @param {String} targetWord 파일명에 포함된 원하는 단어
* @param {Number} targetLine 삽입하고 싶은 라인 번호
* @param {...String} lines 삽입하고 싶은 문자열 (여러 문자열 가능)
* @return {Promise} 수행된 프로미스. resolve에 삽입된 결과 파일의 내용이 담긴다.
*/
const insertLine = (targetDir, targetWord, targetLine, ...lines) => {
return new Promise((resolve, reject) => {
const fileList = readFileList(targetDir);
const filteredList = filterFileList(fileList, targetWord);

filteredList.forEach(fileName => {
const fileContents = readFileContents(fileName, targetDir);
const result = insert(fileContents, targetLine, lines);
fs.writeFile(targetDir + fileName, result, UTF_8, err => {
if (err) console.log('Error :', err);
resolve(result);
});
});
});
}

/**
* @description targetDir에 있는 파일들 중 파일명에 targetWord가 포함되는 파일에 원하는 line을 지운다.
* @param {String} targetDir 원하는 디렉토리의 경로 ex) 'files/'
* @param {String} targetWord 파일명에 포함된 원하는 단어
* @param {Number} targetLine 삭제하고 싶은 라인 번호
* @param {Number} deleteCount 삭제를 원하는 라인 수 (default = 1) optional
* @return {Promise} 수행된 프로미스. resolve에 목표 라인이 제거된 결과 파일의 내용이 담긴다.
*/
const deleteLine = (targetDir, targetWord, targetLine, deleteCount = 1) => {
return new Promise((resolve, reject) => {
const fileList = readFileList(targetDir);
const filteredList = filterFileList(fileList, targetWord);

filteredList.forEach(fileName => {
const fileContents = readFileContents(fileName, targetDir);
const result = remove(fileContents, targetLine, deleteCount)

fs.writeFile(targetDir + fileName, result, UTF_8, err => {
if (err) console.log('Error :', err);
resolve(result);
});
});
});
}

/**
* @description targetDir에 있는 파일들 중 파일명에 targetWord가 포함되는 파일에 원하는 line을 수정한다.
* @param {String} targetDir 원하는 디렉토리의 경로 ex) 'files/'
* @param {String} targetWord 파일명에 포함된 원하는 단어
* @param {Number} targetLine 수정하고 싶은 라인 번호
* @param {String} text 수정하고 싶은 문자열
* @return {Promise} 수행된 프로미스. resolve에 수정된 결과 파일의 내용이 담긴다.
*/
const updateLine = (targetDir, targetWord, targetLine, text) => {
return new Promise((resolve, reject) => {
const fileList = readFileList(targetDir);
const filteredList = filterFileList(fileList, targetWord);

filteredList.forEach(fileName => {
const fileContents = readFileContents(fileName, targetDir);
const result = update(fileContents, targetLine, text)

fs.writeFile(targetDir + fileName, result, UTF_8, err => {
if (err) console.log('Error :', err);
resolve(result);
});
});
});
}

/**
* @description fileName 파일의 원하는 line을 읽어온다.
* @param {String} fileName 읽고자 하는 파일의 경로 ex) 'files/abc.txt'
* @param {Number} targetLine 읽고 싶은 시작 line 번호
* @param {Number} selectCount 읽고자 하는 line 수 (default = 1) optional
* @return {String} 읽어온 결과 문자열
*/
const selectLine = (fileName, targetLine, selectCount = 1) => {
const fileContents = readFileContents(fileName);
const result = select(fileContents, targetLine, selectCount);
return result;
}

module.exports = { insertLine, deleteLine, updateLine, selectLine, NEWLINE };

jest 라이브러리를 사용하여 간단한 테스트도 작성했다.

  • 테스트 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const  { insertLine, deleteLine, updateLine, selectLine, NEWLINE } = require('../lineController');

const WRITE_DIR = 'files/';
const TARGET_WORD = '3085';
const TARGET_FILE = 'files/algorithm3085.md';

const SELECT_LINE_TEST =
`---
layout: post
title: "백준 3085번 사탕 게임"
subtitle: "Baekjoon algorithm"
date: 2020-02-24 09:51:12
author: kwon
categories: algorithm
---`;

describe("test lineEditor", () => {
it("selectLine", () => {
const selected = selectLine(TARGET_FILE, 1, 8);
expect(SELECT_LINE_TEST).toEqual(selected);
})
it("insertLine", async () => {
await insertLine(WRITE_DIR, TARGET_WORD, 1, 'insert', 'insert2');
const selected = selectLine(TARGET_FILE, 1, 2);
expect(`insert${NEWLINE}insert2`).toEqual(selected);
})
it("updateLine", async () => {
await updateLine(WRITE_DIR, TARGET_WORD, 1, 'update');
const selected = selectLine(TARGET_FILE, 1);
expect('update').toEqual(selected);
})
it("deleteLine", async () => {
await deleteLine(WRITE_DIR, TARGET_WORD, 1, 2);
const selected = selectLine(TARGET_FILE, 1);
expect('---').toEqual(selected);
})
})

npm 패키지를 배포해보는 것은 처음이었는데 생각보다 어렵지 않았다. 간단하게 배포 과정을 살펴보자면,

먼저 npm 사이트에 회원가입을 하고 이메일 인증까지 해야한다.

터미널에서 npm login 을 통해 로그인을 하고 npm whoami 를 통해 제대로 로그인이 되었는지 확인할 수 있다.

이제 패키지명을 결정해야 하는데, 처음 생각했던 것은 line-editor 였지만 이미 존재했기 때문에 line-controller 로 결정하게 되었다. 이제 npm init 을 통해 package.json 을 생성하여 패키지명, 버전, 설명, 라이센스 등을 설정해주면 된다.

배포하기 전에 로컬에서 직접 테스트를 해볼 수도 있었는데, npm install [작성한 패키지 디렉토리 경로] 를 하면 로컬에서 내가 만든 패키지를 다운받아 사용해볼 수 있다.

이후 npm publish 명령어를 통해 npm에 배포를 할 수 있었다.

필요에 의해 간단히 만들어본 프로그램이었지만 조금 다듬어서 npm에 배포까지 해보았다. 별거 아닌 것이긴 하지만 처음 패키지를 배포해봤다는 것에 의의를 둬본다.

참조
https://www.daleseo.com/js-npm-publish/
https://heropy.blog/2019/01/31/node-js-npm-module-publish/