funkophile 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -0
- package/funkophileHelpers.js +44 -0
- package/index.js +322 -0
- package/package.json +8 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Funkophile
|
|
2
|
+
|
|
3
|
+
The _func_ tional _file_ processor.
|
|
4
|
+
|
|
5
|
+
### About
|
|
6
|
+
#### *What* is Funkophile?
|
|
7
|
+
|
|
8
|
+
Funkophile is a build tool that inputs files, processes them, and then outputs them. Optionally, it can watch those files for changes, and updating the output files very efficiently. No plugins are needed because what once was configuration is now code- EVERYTHING is done in redux selectors.
|
|
9
|
+
|
|
10
|
+
#### What is Funkophile *not*?
|
|
11
|
+
|
|
12
|
+
- Funkophile is _not_ webpack or rollup. It does not *just* build JS bundles, though it could help you do so.
|
|
13
|
+
- Funkophile is _not_ grunt or gulp. It is not *just* a task runner, it is a file-processor. The "tasks" Funkophile runs are functions run on files.
|
|
14
|
+
- Funkophile is _not_ a module loader.
|
|
15
|
+
- Funkophile is _not necessarily_ for web development. It has a much broader use-case.
|
|
16
|
+
|
|
17
|
+
#### *Why* is Funkophile **awesome**?
|
|
18
|
+
|
|
19
|
+
Funkophile is a functional file processor. It lets you manipulate files in a *functional* way- using redux selectors- and it does so efficiently- using promises. **It lets you focus on the logic of your selectors and disregard the processing of files**. You setup in some files to read, some files to write, and some selectors filled with easily testable logic- Funkophile will handle the borings parts for you! When you first start Funkophile, it will process every file. Subsequent changes to an input file will run through your selectors and automatically update _only_ the dependent output files. It just works! (tm)
|
|
20
|
+
|
|
21
|
+
#### What Funkophile *can do*
|
|
22
|
+
|
|
23
|
+
- Funkophile can replace your flavor-of-the-week State Site Generator with a hackable and lightweight solution.
|
|
24
|
+
- Funkophile can replace some of the things that grunt or gulp do.
|
|
25
|
+
- Funkophile can replace some of the things that webpack is used for.
|
|
26
|
+
- Funkophile can replace both build tools (like grunt and gulp) and bundlers (like webpack and rollup).
|
|
27
|
+
- Funkophile is also very unopinionated and works well with other tools grunt, gulp, webpack, and rollups.
|
|
28
|
+
- Funkophile can functionally and efficiently watch files for changes, process them, then write them back to the filesystem.
|
|
29
|
+
|
|
30
|
+
#### What Funkophile *should not be used for*
|
|
31
|
+
|
|
32
|
+
- Funkophile should only be used in a purely functional way. __Funkophile is asynchronous so that your code does not need to be.__ You should only use Funkophile if you can write purely functional redux selectors. Luckily, these concepts are not that difficult to learn!
|
|
33
|
+
|
|
34
|
+
### Funkophile.config.js
|
|
35
|
+
|
|
36
|
+
Funkophile is configured with Funkophile.config.js. It has 3 sections of note:
|
|
37
|
+
|
|
38
|
+
#### inputs
|
|
39
|
+
`inputs` is a list of configurations defining which files get read and where to store those results in the redux stores
|
|
40
|
+
|
|
41
|
+
#### outputs
|
|
42
|
+
`outputs` is a function which accepts object of selectors, keyed by inputs. These selectors are connected to the redux state and you can use it to handle changes to the redux state, which itself is reacting to changes in the filesystem. This function returns hash object, where the keys are files to write, and the values are the contents of those files.
|
|
43
|
+
|
|
44
|
+
#### selectors
|
|
45
|
+
At the heart of Funkophile is the selector. Within the `outputs` function, you can define any selectors you like, using any JS library you want, provided they are _purely functional_. This means that Funkophile needs no community plugins and can be made to do complex logic cleanly.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const createSelector = require('reselect').createSelector;
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
|
|
5
|
+
contentsOfFiles: (selector) => {
|
|
6
|
+
return createSelector([selector], (selected) => {
|
|
7
|
+
return Object.keys(selected).reduce((mm, k) => mm + selected[k], '')
|
|
8
|
+
})
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
contentOfFile: (selector) => {
|
|
12
|
+
return createSelector([selector], (selected) => {
|
|
13
|
+
try{
|
|
14
|
+
return selected[Object.keys(selected)[0]]
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.error("error", e)
|
|
17
|
+
console.error("selected", selected)
|
|
18
|
+
console.error("selector", selector)
|
|
19
|
+
process.exit(-1)
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
srcAndContentOfFile: (selector, key) => {
|
|
25
|
+
return createSelector([selector], (selected) => {
|
|
26
|
+
return {
|
|
27
|
+
src: key,
|
|
28
|
+
content: selected[key]
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
srcAndContentOfFiles: (selector) => {
|
|
34
|
+
return createSelector([selector], (selected) => {
|
|
35
|
+
const keys = Object.keys(selected)
|
|
36
|
+
return keys.map((key) => {
|
|
37
|
+
return {
|
|
38
|
+
src: key,
|
|
39
|
+
content: selected[key]
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// funkophile/index.js
|
|
2
|
+
|
|
3
|
+
const chokidar = require('chokidar');
|
|
4
|
+
const createSelector = require('reselect').createSelector;
|
|
5
|
+
const createStore = require('redux').createStore;
|
|
6
|
+
const fse = require("fs-extra")
|
|
7
|
+
const glob = require("glob-promise");
|
|
8
|
+
const path = require("path")
|
|
9
|
+
const Promise = require("bluebird")
|
|
10
|
+
|
|
11
|
+
Promise.config({
|
|
12
|
+
cancellation: true
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const funkophileConfig = require(process.argv[2])
|
|
16
|
+
const mode = process.argv[3]
|
|
17
|
+
const keyToWatch = process.argv[4]
|
|
18
|
+
|
|
19
|
+
const INITIALIZE = 'INITIALIZE';
|
|
20
|
+
const UPSERT = 'UPSERT';
|
|
21
|
+
const REMOVE = 'REMOVE';
|
|
22
|
+
|
|
23
|
+
const previousState = {}
|
|
24
|
+
let outputPromise = Promise.resolve();
|
|
25
|
+
|
|
26
|
+
const logger = {
|
|
27
|
+
watchError: (path) => console.log("\u001b[7m ! \u001b[0m" + path),
|
|
28
|
+
watchReady: (path) => console.log("\u001b[7m\u001b[36m < \u001b[0m" + path),
|
|
29
|
+
watchAdd: (path) => console.log("\u001b[7m\u001b[34m + \u001b[0m./" + path),
|
|
30
|
+
watchChange: (path) => console.log("\u001b[7m\u001b[35m * \u001b[0m" + path),
|
|
31
|
+
watchUnlink: (path) => console.log("\u001b[7m\u001b[31m - \u001b[0m./" + path),
|
|
32
|
+
|
|
33
|
+
stateChange: () => console.log("\u001b[7m\u001b[31m --- Redux state changed --- \u001b[0m"),
|
|
34
|
+
|
|
35
|
+
cleaningEmptyfolder: (path) => console.log("\u001b[31m\u001b[7m XXX! \u001b[0m" + path),
|
|
36
|
+
|
|
37
|
+
readingFile: (path) => console.log("\u001b[31m <-- \u001b[0m" + path),
|
|
38
|
+
removedFile: (path) => console.log("\u001b[31m\u001b[7m ??? \u001b[0m./" + path),
|
|
39
|
+
|
|
40
|
+
writingString: (path) => console.log("\u001b[32m --> \u001b[0m" + path),
|
|
41
|
+
writingFunction: (path) => console.log("\u001b[33m ... \u001b[0m" + path),
|
|
42
|
+
writingPromise: (path) => console.log("\u001b[33m ... \u001b[0m" + path),
|
|
43
|
+
writingError: (path, message) => console.log("\u001b[31m !!! \u001b[0m" + path + " " + message),
|
|
44
|
+
|
|
45
|
+
waiting: () => console.log("\u001b[7m Funkophile is done for now but waiting on changes...\u001b[0m "),
|
|
46
|
+
done: () => console.log("\u001b[7m Funkophile is done!\u001b[0m ")
|
|
47
|
+
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function cleanEmptyFoldersRecursively(folder) {
|
|
51
|
+
var isDir = fs.statSync(folder).isDirectory();
|
|
52
|
+
if (!isDir) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
var files = fs.readdirSync(folder);
|
|
56
|
+
if (files.length > 0) {
|
|
57
|
+
files.forEach(function(file) {
|
|
58
|
+
var fullPath = path.join(folder, file);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// re-evaluate files; after deleting subfolder
|
|
62
|
+
// we may have parent folder empty now
|
|
63
|
+
files = fs.readdirSync(folder);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (files.length == 0) {
|
|
67
|
+
logger.cleaningEmptyfolder(folder)
|
|
68
|
+
|
|
69
|
+
fs.rmdirSync(folder);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const dispatchUpsert = (store, key, file, encodings) => {
|
|
75
|
+
logger.readingFile(file)
|
|
76
|
+
store.dispatch({
|
|
77
|
+
type: UPSERT,
|
|
78
|
+
payload: {
|
|
79
|
+
key: key,
|
|
80
|
+
src: file,
|
|
81
|
+
contents: fse.readFileSync(file, Object.keys(encodings).find((e) => encodings[e].includes(file.split('.')[2])))
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// const filetype = file.split('.')[2]
|
|
86
|
+
// const encoding = Object.keys(encodings).find((e) => encodings[e].includes(filetype))
|
|
87
|
+
// const relativeFilePath = './' + file;
|
|
88
|
+
// console.log("\u001b[31m <-- \u001b[0m" + file)
|
|
89
|
+
// fse.readFile(file, encoding).then((contents) => {
|
|
90
|
+
// store.dispatch({
|
|
91
|
+
// type: UPSERT,
|
|
92
|
+
// payload: {
|
|
93
|
+
// key: key,
|
|
94
|
+
// src: file,
|
|
95
|
+
// contents: contents
|
|
96
|
+
// }
|
|
97
|
+
// });
|
|
98
|
+
// });
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
function omit(key, obj) {
|
|
104
|
+
const {
|
|
105
|
+
[key]: omitted, ...rest
|
|
106
|
+
} = obj;
|
|
107
|
+
return rest;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const store = createStore((state = {
|
|
111
|
+
initialLoad: true,
|
|
112
|
+
...funkophileConfig.initialState,
|
|
113
|
+
timestamp: Date.now()
|
|
114
|
+
}, action) => {
|
|
115
|
+
// console.log("\u001b[7m\u001b[35m ||| Redux recieved action \u001b[0m", action.type)
|
|
116
|
+
if (!action.type.includes('@@redux')) {
|
|
117
|
+
|
|
118
|
+
if (action.type === INITIALIZE) {
|
|
119
|
+
return {
|
|
120
|
+
...state,
|
|
121
|
+
initialLoad: false,
|
|
122
|
+
timestamp: Date.now()
|
|
123
|
+
}
|
|
124
|
+
} else if (action.type === UPSERT) {
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
[action.payload.key]: {
|
|
128
|
+
...state[action.payload.key],
|
|
129
|
+
...{
|
|
130
|
+
[action.payload.src]: action.payload.contents
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
timestamp: Date.now()
|
|
134
|
+
}
|
|
135
|
+
} else if (action.type === REMOVE) {
|
|
136
|
+
return {
|
|
137
|
+
...state,
|
|
138
|
+
[action.payload.key]: omit(action.payload.file, state[action.payload.key]),
|
|
139
|
+
timestamp: Date.now()
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
console.error("Redux was asked to handle an unknown action type: " + action.type)
|
|
143
|
+
process.exit(-1)
|
|
144
|
+
}
|
|
145
|
+
return state
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const finalSelector = funkophileConfig.outputs(Object.keys(funkophileConfig.inputs).reduce((mm, inputKey) => {
|
|
150
|
+
return {
|
|
151
|
+
...mm,
|
|
152
|
+
[inputKey]: createSelector([(x) => x], (root) => root[inputKey])
|
|
153
|
+
}
|
|
154
|
+
}, {}))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
// Wait for all the file watchers to check in
|
|
158
|
+
Promise.all(
|
|
159
|
+
Object.keys(funkophileConfig.inputs)
|
|
160
|
+
// ['FUNKYBUNDLE']
|
|
161
|
+
.map((inputRuleKey) => {
|
|
162
|
+
const path = `./${funkophileConfig.options.inFolder}/${funkophileConfig.inputs[inputRuleKey] || ''}`
|
|
163
|
+
return new Promise((fulfill, reject) => {
|
|
164
|
+
if (mode === "build") {
|
|
165
|
+
glob(path, {}).then((files) => {
|
|
166
|
+
files.forEach((file) => {
|
|
167
|
+
dispatchUpsert(store, inputRuleKey, file, funkophileConfig.encodings);
|
|
168
|
+
})
|
|
169
|
+
}).then(() => {
|
|
170
|
+
fulfill()
|
|
171
|
+
})
|
|
172
|
+
} else if (mode === "watch") {
|
|
173
|
+
|
|
174
|
+
chokidar.watch(path, {})
|
|
175
|
+
.on('error', error => {
|
|
176
|
+
logger.watchError(path)
|
|
177
|
+
})
|
|
178
|
+
.on('ready', () => {
|
|
179
|
+
logger.watchReady(path)
|
|
180
|
+
fulfill()
|
|
181
|
+
})
|
|
182
|
+
.on('add', path => {
|
|
183
|
+
logger.watchAdd(path)
|
|
184
|
+
dispatchUpsert(store, inputRuleKey, './' + path, funkophileConfig.encodings);
|
|
185
|
+
})
|
|
186
|
+
.on('change', path => {
|
|
187
|
+
logger.watchChange(path)
|
|
188
|
+
dispatchUpsert(store, inputRuleKey, './' + path, funkophileConfig.encodings);
|
|
189
|
+
})
|
|
190
|
+
.on('unlink', path => {
|
|
191
|
+
logger.watchUnlink(path)
|
|
192
|
+
store.dispatch({
|
|
193
|
+
type: REMOVE,
|
|
194
|
+
payload: {
|
|
195
|
+
key: inputRuleKey,
|
|
196
|
+
file: './' + path
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
.on('unlinkDir', path => {
|
|
201
|
+
logger.watchUnlink(path)
|
|
202
|
+
})
|
|
203
|
+
// .on('raw', (event, path, details) => { // internal
|
|
204
|
+
// log('Raw event info:', event, path, details);
|
|
205
|
+
// })
|
|
206
|
+
|
|
207
|
+
} else {
|
|
208
|
+
console.error(`The 3rd argument should be 'watch' or 'build', not "${mode}"`)
|
|
209
|
+
process.exit(-1)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
});
|
|
213
|
+
})).then(function() {
|
|
214
|
+
|
|
215
|
+
// listen for changes to the store
|
|
216
|
+
store.subscribe(() => {
|
|
217
|
+
logger.stateChange()
|
|
218
|
+
const outputs = finalSelector(store.getState())
|
|
219
|
+
|
|
220
|
+
if (outputPromise.isPending()) {
|
|
221
|
+
console.log('cancelling previous write!')
|
|
222
|
+
outputPromise.cancel()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
outputPromise = Promise.all(
|
|
226
|
+
Array.from(new Set(Object.keys(previousState).concat(Object.keys(outputs))))
|
|
227
|
+
.map((key) => {
|
|
228
|
+
|
|
229
|
+
return new Promise((fulfill, reject) => {
|
|
230
|
+
if (!outputs[key]) {
|
|
231
|
+
|
|
232
|
+
const file = funkophileConfig.options.outFolder + "/" + key
|
|
233
|
+
logger.removedFile(file)
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
fse.unlinkSync('./' + file)
|
|
237
|
+
cleanEmptyFoldersRecursively('./' + file.substring(0, file.lastIndexOf("/")))
|
|
238
|
+
} catch (ex) {
|
|
239
|
+
// console.error('inner', ex.message);
|
|
240
|
+
// throw ex;
|
|
241
|
+
} finally {
|
|
242
|
+
// console.log('finally');
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
delete previousState[key]
|
|
246
|
+
fulfill()
|
|
247
|
+
} else {
|
|
248
|
+
if (outputs[key] !== previousState[key]) {
|
|
249
|
+
previousState[key] = outputs[key]
|
|
250
|
+
|
|
251
|
+
const relativeFilePath = './' + funkophileConfig.options.outFolder + "/" + key;
|
|
252
|
+
const contents = outputs[key];
|
|
253
|
+
|
|
254
|
+
if (typeof contents === "function") {
|
|
255
|
+
logger.writingFunction(relativeFilePath)
|
|
256
|
+
contents((err, res) => {
|
|
257
|
+
fse.outputFile(relativeFilePath, res, fulfill);
|
|
258
|
+
logger.writingString(relativeFilePath);
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
} else if (typeof contents === 'string') {
|
|
262
|
+
fse.outputFile(relativeFilePath, contents, fulfill);
|
|
263
|
+
logger.writingString(relativeFilePath);
|
|
264
|
+
|
|
265
|
+
} else if (Buffer.isBuffer(contents)) {
|
|
266
|
+
fse.outputFile(relativeFilePath, contents, fulfill);
|
|
267
|
+
logger.writingString(relativeFilePath);
|
|
268
|
+
|
|
269
|
+
} else if (Array.isArray(contents)) {
|
|
270
|
+
fse.outputFile(relativeFilePath, JSON.stringify(contents), fulfill);
|
|
271
|
+
logger.writingString(relativeFilePath);
|
|
272
|
+
|
|
273
|
+
} else if (typeof contents.then === 'function') {
|
|
274
|
+
logger.writingPromise(relativeFilePath)
|
|
275
|
+
Promise.resolve(contents).then(function(value) {
|
|
276
|
+
|
|
277
|
+
if (value instanceof Error) {
|
|
278
|
+
logger.writingError(relativeFilePath, value.message)
|
|
279
|
+
} else {
|
|
280
|
+
fse.outputFile(relativeFilePath, value, fulfill);
|
|
281
|
+
logger.writingString(relativeFilePath);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
}, function(value) {
|
|
285
|
+
// not called
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
} else {
|
|
289
|
+
console.log("I don't recognize what this is but I will try to write it to a file: " + relativeFilePath, contents)
|
|
290
|
+
fse.outputFile(relativeFilePath, contents, callback);
|
|
291
|
+
logger.writingString(relativeFilePath);
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
fulfill()
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
})
|
|
300
|
+
).then(() => {
|
|
301
|
+
cleanEmptyFoldersRecursively(funkophileConfig.options.outFolder);
|
|
302
|
+
|
|
303
|
+
if (mode === "build") {
|
|
304
|
+
logger.done()
|
|
305
|
+
} else if (mode === "watch") {
|
|
306
|
+
logger.waiting()
|
|
307
|
+
} else {
|
|
308
|
+
console.error(`The 3rd argument should be 'watch' or 'build', not "${mode}"`)
|
|
309
|
+
process.exit(-1)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
})
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// lastly, turn the store `on`.
|
|
316
|
+
// This is to prevent unecessary recomputations when initialy adding files to redux
|
|
317
|
+
store.dispatch({
|
|
318
|
+
type: INITIALIZE,
|
|
319
|
+
payload: true
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
})
|