UNPKG

6.09 kBJavaScriptView Raw
1var fs = require('fs/promises')
2var path = require("path")
3var jeye = require("jeye")
4var ora = require("ora")
5var chalk = require("chalk")
6var { premove } = require("premove")
7var { mkdir } = require("mk-dirs/sync")
8const { extname } = require("path")
9require = require("esm")(module)
10
11class Spinner{
12 constructor(logo_art='◸/◿', loading_art=['◸-◿','◸\\◿','◸|◿','◸/◿'], error_art='◸x◿', success_art='◸✓◿'){
13 Object.assign(this, { logo_art, error_art, success_art })
14 this.s = ora({
15 text: "initializing...",
16 spinner: {
17 interval: 80,
18 frames: loading_art
19 },
20 color: 'yellow'
21 })
22 }
23 ready(text="ready"){
24 this.s.stopAndPersist({
25 symbol: chalk.green(this.logo_art),
26 text
27 })
28 }
29 start(text="loading..."){
30 this.s.start(text)
31 }
32 error(text="generic routo error"){
33 this.s.stopAndPersist({
34 symbol: chalk.red(this.error_art),
35 text
36 })
37 }
38 success(text="success"){
39 this.s.stopAndPersist({
40 symbol: chalk.green(this.success_art),
41 text
42 })
43 }
44}
45
46class Routo{
47 constructor(sources, destination, { ignore, watch, silent, config }){
48 Object.assign(this, { sources, destination, silent, ignore })
49 let configData = {}
50 if(config && typeof config === 'string'){
51 try{
52 configData = require(path.join(process.cwd(), config)).default
53 } catch(e){
54 this.error("Invalid config path")
55 }
56 }
57 this.config = {
58 builders: [],
59 transforms: [],
60 ...configData
61 }
62 this.builders = [...this.config.builders, {
63 match: (p) => {
64 // returns true for style.css.js, false for robots.txt
65 return /([\w-*]+)(\.[\w-*]+\.js)/g.exec(p)
66 },
67 build: async (p, { id }) => {
68 let abs_path = path.join(process.cwd(), p)
69 delete require.cache[abs_path]
70 let data = await Promise.resolve(require(abs_path).default)
71 // remove .js at end
72 id = id.substr(0,id.length - 3)
73 // if .html other than the first index, put in a folder
74 if(extname(id) === '.html' && id !== '/index.html'){
75 id = id.replace('.html', '/index.html')
76 }
77 return {
78 [id]: data
79 }
80 }
81 }]
82 this.transforms = this.config.builders || []
83 if(watch){
84 this.spinner = new Spinner()
85 this.watch()
86 }
87 }
88
89 watch(){
90 this.spinner.start('initializing routo...')
91 jeye.watch(this.sources, { ignore: this.ignore })
92 .on("change", async (p, info, changed=[]) => {
93 changed.forEach(c => {
94 let abs_path = path.join(process.cwd(), c)
95 delete require.cache[abs_path]
96 });
97 if(!this.silent && !this.spinner.isSpinning){
98 this.spinner.start('building...')
99 this.loadStart = Date.now()
100 }
101 try{
102 await this.buildFile(p, info)
103 } catch(e){
104 this.error("Error building file: " + p)
105 console.log(e)
106 }
107 return
108 })
109 .on("aggregate", async (targets, changed) => {
110 try{
111 // await this.aggregate(targets, changed)
112 } catch(e){
113 this.error("Error with aggregate build")
114 console.log(e)
115 return;
116 }
117 if(!this.silent){
118 let elapsed = Date.now() - this.loadStart
119 let succeedMessage = () => this.spinner.success(`${changed.length} file${changed.length === 1 ? '' : 's'} changed in ${elapsed}ms`)
120 if(elapsed < 300){
121 setTimeout(succeedMessage, 300 - elapsed)
122 } else {
123 succeedMessage()
124 }
125 }
126 return
127 })
128 .on("ready", (targets) => {
129 this.build().then(() => {
130 if(!this.silent){
131 this.spinner.ready('routo is ready')
132 }
133 })
134 })
135 .on("error", (message) => {
136 this.error(message)
137 })
138 .on("remove", (p) => {
139 // TODO: get list of associated output paths from this source p
140 // TODO: recursivevly ascend parent directories and delete if empty
141 // premove(this.getOutputPaths(p))
142 })
143 }
144
145 error(message){
146 if(!this.silent){
147 if(this.spinner){
148 this.spinner.error(message)
149 } else {
150 console.log(`${chalk.red('◸x◿')} ${message}`)
151 }
152 } else {
153 throw message;
154 }
155 }
156
157 isBuilder(p){
158 let segments = path.basename(p).split('.')
159 return segments.length > 2 && segments[segments.length - 1] === 'js'
160 }
161
162 async buildFile(p, info){
163 // is this a JS file or a raw file to copy over?
164 // which builder should we use?
165 async function copyBuilder(p, {id}){
166 let data = await fs.readFile(p)
167 return { [id]: data }
168 }
169
170 // use builder to build file or use readFile contents
171 let builder = this.builders.find(b =>
172 (!b.match || (typeof b.match === 'function' && b.match(p)))
173 && typeof b.build === 'function'
174 )
175 builder = builder ? builder.build : copyBuilder
176 let output = await builder(p, info)
177 let promises = Object.keys(output).map(async k => {
178 let output_path = path.join(this.destination, k)
179 let ensured_folder = output_path.replace(path.basename(output_path),"")
180 mkdir(ensured_folder)
181 await fs.writeFile(output_path, output[k])
182 })
183
184 await Promise.all(promises)
185 return;
186 }
187
188 async aggregate(targets, changed = null){
189 if(changed === null){
190 // assume all have changed
191 } else {
192 // only some files have changed
193 }
194 return;
195 }
196
197 async build(){
198 try{
199 let targets = await jeye.targets(this.sources, { ignore: this.ignore })
200 await Promise.all(
201 Object.keys(targets).map(async p => {
202 try{
203 await this.buildFile(p, targets[p])
204 } catch(e){
205 this.error(p + ": " + e)
206 }
207 })
208 )
209 await this.aggregate(targets)
210 return Object.keys(targets)
211 } catch(e){
212 this.error(e)
213 }
214 }
215
216}
217
218module.exports = Routo
\No newline at end of file