funkophile 0.2.4 → 1.0.0

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/test-utils.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { AppState, FileContents, FunkophileConfig } from './core';
2
+
3
+ export const createMockState = (partial?: Partial<AppState>): AppState => ({
4
+ timestamp: Date.now(),
5
+ posts: {
6
+ 'post1.md': '# Post 1',
7
+ 'post2.md': '# Post 2',
8
+ 'post33.md': 'hello world'
9
+ },
10
+ pages: {
11
+ 'about.md': '# About'
12
+ },
13
+ ...partial
14
+ });
15
+
16
+ export const createMockConfig = (partial?: Partial<FunkophileConfig>): FunkophileConfig => ({
17
+ mode: 'build',
18
+ initialState: {
19
+ posts: {
20
+ 'post1.md': '# Post 1',
21
+ 'post2.md': '# Post 2'
22
+ },
23
+ pages: {
24
+ 'about.md': '# About'
25
+ }
26
+ },
27
+ options: {
28
+ inFolder: 'src',
29
+ outFolder: 'dist'
30
+ },
31
+ encodings: {
32
+ utf8: ['md', 'txt']
33
+ },
34
+ inputs: {
35
+ posts: 'posts/**/*.md',
36
+ pages: 'pages/**/*.md'
37
+ },
38
+ outputs: () => ({}),
39
+ ...partial
40
+ });
package/tsconfig.json CHANGED
@@ -1,5 +1,8 @@
1
1
  {
2
2
  "compilerOptions": {
3
+
4
+ "allowJs": true,
5
+
3
6
  "module": "ESNext",
4
7
  "target": "ESNext",
5
8
  "moduleResolution": "node",
@@ -8,8 +11,18 @@
8
11
  "declaration": true,
9
12
  "strict": true,
10
13
  "rootDir": ".",
11
- "noImplicitAny": false
14
+ "noImplicitAny": false,
15
+ "useUnknownInCatchVariables": false,
16
+ "emitDeclarationOnly": true,
17
+ "skipLibCheck": true
12
18
  },
13
- "include": ["*.ts"],
14
- "exclude": ["node_modules"]
15
- }
19
+ "include": [
20
+ "index.ts",
21
+ "funkophileHelpers.ts",
22
+ "utils.ts"
23
+ ],
24
+ "exclude": [
25
+ "node_modules",
26
+ "dist"
27
+ ]
28
+ }
package/utils.ts ADDED
@@ -0,0 +1,539 @@
1
+ import fs from "fs";
2
+ import fse from "fs-extra";
3
+ import http from "http";
4
+ import path from "path";
5
+ import { Action, createStore, Store } from "redux";
6
+ import { createSelector } from "reselect";
7
+ import url from "url";
8
+ import { glob } from "glob";
9
+ import chokidar from "chokidar";
10
+
11
+ export type IConfig = {
12
+ mode: "build" | "watch";
13
+ initialState: any;
14
+ options: {
15
+ inFolder: string;
16
+ outFolder: string;
17
+ port?: number;
18
+ };
19
+ encodings: Record<string, string[]>;
20
+ inputs: Record<string, string>;
21
+ outputs: (x: any) => any;
22
+ };
23
+
24
+ export const INITIALIZE = "INITIALIZE";
25
+ export const UPSERT = "UPSERT";
26
+ export const REMOVE = "REMOVE";
27
+
28
+ export const previousState: any = {};
29
+
30
+ export const logger = {
31
+ watchError: (p: string) => console.log("\u001b[7m ! \u001b[0m" + p),
32
+ watchReady: (p: string) =>
33
+ console.log("\u001b[7m\u001b[36m < \u001b[0m" + p),
34
+ watchAdd: (p: string) =>
35
+ console.log("\u001b[7m\u001b[34m + \u001b[0m./" + p),
36
+ watchChange: (p: string) =>
37
+ console.log("\u001b[7m\u001b[35m * \u001b[0m" + p),
38
+ watchUnlink: (p: string) =>
39
+ console.log("\u001b[7m\u001b[31m - \u001b[0m./" + p),
40
+ stateChange: () =>
41
+ console.log("\u001b[7m\u001b[31m --- Redux state changed --- \u001b[0m"),
42
+ cleaningEmptyfolder: (p: string) =>
43
+ console.log("\u001b[31m\u001b[7m XXX! \u001b[0m" + p),
44
+ readingFile: (p: string) => console.log("\u001b[31m <-- \u001b[0m" + p),
45
+ removedFile: (p: string) =>
46
+ console.log("\u001b[31m\u001b[7m ??? \u001b[0m./" + p),
47
+ writingString: (p: string) => console.log("\u001b[32m --> \u001b[0m" + p),
48
+ writingFunction: (p: string) => console.log("\u001b[33m ... \u001b[0m" + p),
49
+ writingPromise: (p: string) => console.log("\u001b[33m ... \u001b[0m" + p),
50
+ writingError: (p: string, message: string) =>
51
+ console.log("\u001b[31m !!! \u001b[0m" + p + " " + message),
52
+
53
+ waiting: () =>
54
+ console.log(
55
+ "\u001b[7m Funkophile is done for now but waiting on changes...\u001b[0m "
56
+ ),
57
+ done: () => console.log("\u001b[7m Funkophile is done!\u001b[0m "),
58
+ };
59
+
60
+ export function cleanEmptyFoldersRecursively(folder: string) {
61
+ var isDir = fs.statSync(folder).isDirectory();
62
+ if (!isDir) {
63
+ return;
64
+ }
65
+ var files = fs.readdirSync(folder);
66
+ if (files.length > 0) {
67
+ files.forEach(function (file) {
68
+ var fullPath = path.join(folder, file);
69
+ });
70
+
71
+ // re-evaluate files; after deleting subfolder
72
+ // we may have parent folder empty now
73
+ files = fs.readdirSync(folder);
74
+ }
75
+
76
+ if (files.length == 0) {
77
+ logger.cleaningEmptyfolder(folder);
78
+
79
+ fs.rmdirSync(folder);
80
+ return;
81
+ }
82
+ }
83
+
84
+ export const dispatchUpsert = (
85
+ store: Store,
86
+ key: string,
87
+ file: string,
88
+ encodings: Record<string, string[]>
89
+ ) => {
90
+ const fileType: string = path.basename(file).split(".")[1];
91
+
92
+ let encoding: BufferEncoding = Object.keys(encodings).find((e) => {
93
+ return encodings[e].includes(fileType);
94
+ }) as BufferEncoding;
95
+
96
+ logger.readingFile(file);
97
+ store.dispatch({
98
+ type: UPSERT,
99
+ payload: {
100
+ key: key,
101
+ // key: path.relative(process.cwd(), key),
102
+ src: file,
103
+ contents: fse.readFileSync(file, encoding),
104
+ },
105
+ });
106
+ };
107
+
108
+ export function omit(key: string, obj: any) {
109
+ const { [key]: omitted, ...rest } = obj;
110
+ return rest;
111
+ }
112
+
113
+ export function newStore(funkophileConfig): Store<any, Action<string>, any> {
114
+ const initialInputState = Object.keys(funkophileConfig.inputs).reduce(
115
+ (state, inputKey) => {
116
+ state[inputKey] = {};
117
+ return state;
118
+ },
119
+ {} as Record<string, any>
120
+ );
121
+
122
+ return createStore(
123
+ (
124
+ state = {
125
+ initialLoad: true,
126
+ ...initialInputState,
127
+ ...funkophileConfig.initialState,
128
+ timestamp: Date.now(),
129
+ },
130
+ action
131
+ ) => {
132
+ if (state === undefined) {
133
+ throw new Error("Redux state is undefined. This should never happen.");
134
+ }
135
+ console.log(
136
+ `\u001b[35m\u001b[1m[Funkophile]\u001b[0m Redux received action: ${action.type}`
137
+ );
138
+ if (!action.type.includes("@@redux")) {
139
+ if (action.type === INITIALIZE) {
140
+ console.log(
141
+ `\u001b[35m\u001b[1m[Funkophile]\u001b[0m INITIALIZE action - setting initialLoad to false`
142
+ );
143
+ return {
144
+ ...state,
145
+ initialLoad: false,
146
+ timestamp: Date.now(),
147
+ };
148
+ } else if (action.type === UPSERT) {
149
+ console.log(
150
+ `\u001b[35m\u001b[1m[Funkophile]\u001b[0m UPSERT action for key: ${action["payload"].key}, file: ${action["payload"].src}`
151
+ );
152
+ return {
153
+ ...state,
154
+ [action["payload"].key]: {
155
+ // @ts-ignore
156
+ ...state[action.payload.key],
157
+ ...{
158
+ [action["payload"].src]: action["payload"].contents,
159
+ },
160
+ },
161
+ timestamp: Date.now(),
162
+ };
163
+ } else if (action.type === REMOVE) {
164
+ console.log(
165
+ `\u001b[35m\u001b[1m[Funkophile]\u001b[0m REMOVE action for key: ${action["payload"].key}, file: ${action["payload"].file}`
166
+ );
167
+ // Ensure the key exists before trying to omit from it
168
+ const currentKeyState = state[action["payload"].key] || {};
169
+ return {
170
+ ...state,
171
+ [action["payload"].key]: omit(
172
+ action["payload"].file,
173
+ currentKeyState
174
+ ),
175
+ timestamp: Date.now(),
176
+ };
177
+ } else {
178
+ console.error(
179
+ "Redux was asked to handle an unknown action type: " + action.type
180
+ );
181
+ process.exit(-1);
182
+ }
183
+ // return state
184
+ }
185
+ return state;
186
+ }
187
+ );
188
+ }
189
+
190
+ export function makeFinalSelector(funkophileConfig) {
191
+ return funkophileConfig.outputs(
192
+ Object.keys(funkophileConfig.inputs).reduce((mm, inputKey) => {
193
+ return {
194
+ ...mm,
195
+ [inputKey]: createSelector([(x) => x], (root) => {
196
+ // The input key should always be present now, even if it's an empty object
197
+ const result = root[inputKey];
198
+ // If result is undefined, it's a programming error since we initialize all input keys
199
+ if (result === undefined) {
200
+ console.warn(
201
+ `\u001b[33m\u001b[1m[Funkophile]\u001b[0m Input key "${inputKey}" is undefined in state, which shouldn't happen. Using empty object.`
202
+ );
203
+ return {};
204
+ }
205
+ return result;
206
+ }),
207
+ };
208
+ }, {})
209
+ );
210
+ }
211
+
212
+ export function startServing(funkophileConfig): void {
213
+ const port = funkophileConfig.options.port || 8080;
214
+ const server = http.createServer((req, res) => {
215
+ if (!req.url) {
216
+ res.statusCode = 400;
217
+ res.end("Bad Request");
218
+ return;
219
+ }
220
+
221
+ const parsedUrl = url.parse(req.url);
222
+ let pathname = parsedUrl.pathname;
223
+
224
+ // Default to index.html if the path ends with /
225
+ if (pathname && pathname.endsWith("/")) {
226
+ pathname += "index.html";
227
+ }
228
+
229
+ // Remove leading slash
230
+ const filePath = pathname ? pathname.substring(1) : "index.html";
231
+
232
+ // Construct the full path to the file
233
+ const fullPath = path.join(
234
+ process.cwd(),
235
+ funkophileConfig.options.outFolder,
236
+ filePath
237
+ );
238
+
239
+ // Check if file exists
240
+ fs.access(fullPath, fs.constants.F_OK, (err) => {
241
+ if (err) {
242
+ // Try with .html extension
243
+ const htmlPath = fullPath + ".html";
244
+ fs.access(htmlPath, fs.constants.F_OK, (htmlErr) => {
245
+ if (htmlErr) {
246
+ // File not found
247
+ res.statusCode = 404;
248
+ res.end("File not found");
249
+ } else {
250
+ // Serve the .html file
251
+ fs.readFile(htmlPath, (readErr, data) => {
252
+ if (readErr) {
253
+ res.statusCode = 500;
254
+ res.end("Internal Server Error");
255
+ } else {
256
+ res.setHeader("Content-Type", "text/html");
257
+ res.end(data);
258
+ }
259
+ });
260
+ }
261
+ });
262
+ } else {
263
+ // Serve the file
264
+ fs.readFile(fullPath, (readErr, data) => {
265
+ if (readErr) {
266
+ res.statusCode = 500;
267
+ res.end("Internal Server Error");
268
+ } else {
269
+ // Set appropriate content type based on file extension
270
+ const ext = path.extname(fullPath).toLowerCase();
271
+ const contentTypes: Record<string, string> = {
272
+ ".html": "text/html",
273
+ ".css": "text/css",
274
+ ".js": "application/javascript",
275
+ ".json": "application/json",
276
+ ".png": "image/png",
277
+ ".jpg": "image/jpeg",
278
+ ".jpeg": "image/jpeg",
279
+ ".gif": "image/gif",
280
+ ".svg": "image/svg+xml",
281
+ ".ico": "image/x-icon",
282
+ };
283
+ res.setHeader(
284
+ "Content-Type",
285
+ contentTypes[ext] || "application/octet-stream"
286
+ );
287
+ res.end(data);
288
+ }
289
+ });
290
+ }
291
+ });
292
+ });
293
+
294
+ server.listen(port, () => {
295
+ console.log(
296
+ `\u001b[36m\u001b[1m[Funkophile]\u001b[0m Server running at http://localhost:${port}/`
297
+ );
298
+ });
299
+
300
+ // Handle process exit to close the server
301
+ process.on("SIGINT", () => {
302
+ if (server) {
303
+ server.close();
304
+ }
305
+ // process.exit(0);
306
+ });
307
+ }
308
+
309
+ // Log all input keys to see if they're present
310
+ export function logInputKeys(funkophileConfig, currentState) {
311
+ Object.keys(funkophileConfig.inputs).forEach((inputKey) => {
312
+ if (currentState[inputKey]) {
313
+ console.log(
314
+ `\u001b[36m\u001b[1m[Funkophile]\u001b[0m Input key "${inputKey}" found in state with ${
315
+ Object.keys(currentState[inputKey]).length
316
+ } files`
317
+ );
318
+ // Only log file names if there are files to avoid cluttering the output
319
+ if (Object.keys(currentState[inputKey]).length > 0) {
320
+ console.log(
321
+ `\u001b[36m\u001b[1m[Funkophile]\u001b[0m Files for "${inputKey}":`,
322
+ Object.keys(currentState[inputKey])
323
+ );
324
+ }
325
+ } else {
326
+ console.warn(
327
+ `\u001b[33m\u001b[1m[Funkophile]\u001b[0m Input key "${inputKey}" NOT found in state`
328
+ );
329
+ }
330
+ });
331
+ }
332
+
333
+ export function logDone(funkophileConfig, currentState) {
334
+ if (funkophileConfig.mode === "build") {
335
+ console.log(
336
+ "\u001b[32m\u001b[1m[Funkophile]\u001b[0m Build completed successfully!"
337
+ );
338
+ logger.done();
339
+ } else if (funkophileConfig.mode === "watch") {
340
+ console.log(
341
+ "\u001b[36m\u001b[1m[Funkophile]\u001b[0m Watching for file changes..."
342
+ );
343
+ // Log the localhost URL if port is specified
344
+ const port = funkophileConfig.options.port || 8080;
345
+ console.log(
346
+ `\u001b[36m\u001b[1m[Funkophile]\u001b[0m Serving at: http://localhost:${port}/`
347
+ );
348
+ logger.waiting();
349
+ } else {
350
+ throw `\u001b[31m\u001b[1m[Funkophile]\u001b[0m The mode should be 'watch' or 'build', not "${funkophileConfig.mode}"`;
351
+ }
352
+ }
353
+
354
+ export function makePromissesArray(funkophileConfig, store) {
355
+ return Object.keys(funkophileConfig.inputs).map((inputRuleKey) => {
356
+ // Ensure the pattern includes the inFolder and is relative to the current working directory
357
+ // Also, make sure to handle patterns that might already include the inFolder
358
+ const pattern = funkophileConfig.inputs[inputRuleKey] || "";
359
+ // For glob, we want the pattern to be relative to process.cwd()
360
+ // Join inFolder and pattern using forward slashes
361
+ const globPattern = path.posix.join(
362
+ funkophileConfig.options.inFolder,
363
+ pattern
364
+ );
365
+ // console.log(`[Funkophile] Looking for files with glob pattern: ${globPattern}`);
366
+ // console.log(`[Funkophile] Current working directory: ${process.cwd()}`);
367
+
368
+ return new Promise<void>((fulfill, reject) => {
369
+ if (funkophileConfig.mode === "build") {
370
+ // Use the glob pattern we constructed earlier
371
+ // console.log(`[Funkophile] Searching for files matching pattern: ${globPattern}`);
372
+ // console.log(`[Funkophile] Input rule key: ${inputRuleKey}`);
373
+
374
+ glob(globPattern, { cwd: process.cwd() })
375
+ .then((files: string[]) => {
376
+ // console.log(`[Funkophile] Found ${files.length} files for ${inputRuleKey} (pattern: ${pattern}):`, files);
377
+ if (files.length === 0) {
378
+ console.warn(
379
+ `No files found for input key "${inputRuleKey}" with pattern "${globPattern}"`
380
+ );
381
+ // Even if no files are found, the key is already initialized in the state
382
+ // No need to dispatch anything
383
+ } else {
384
+ files.forEach((file) => {
385
+ // Make sure the file path is absolute
386
+ const absoluteFilePath = path.resolve(process.cwd(), file);
387
+ // console.log(`[Funkophile] Adding file to state for key ${inputRuleKey}: ${absoluteFilePath}`);
388
+ dispatchUpsert(
389
+ store,
390
+ inputRuleKey,
391
+ absoluteFilePath,
392
+ funkophileConfig.encodings
393
+ );
394
+ });
395
+ }
396
+ })
397
+ .then(() => {
398
+ fulfill();
399
+ })
400
+ .catch((error) => {
401
+ // console.error(`[Funkophile] Error globbing for pattern ${globPattern}:`, error);
402
+ reject(error);
403
+ });
404
+ } else if (funkophileConfig.mode === "watch") {
405
+ console.log(
406
+ `\u001b[36m\u001b[1m[Funkophile]\u001b[0m Setting up watcher for pattern: ${globPattern}`
407
+ );
408
+ console.log(
409
+ `\u001b[36m\u001b[1m[Funkophile]\u001b[0m Current working directory: ${process.cwd()}`
410
+ );
411
+
412
+ // First, process initial files using glob to ensure all files are loaded
413
+ glob(globPattern, { cwd: process.cwd() })
414
+ .then((files: string[]) => {
415
+ console.log(
416
+ `\u001b[36m\u001b[1m[Funkophile]\u001b[0m Found ${files.length} initial files for ${inputRuleKey}`
417
+ );
418
+ files.forEach((file) => {
419
+ const absoluteFilePath = path.resolve(process.cwd(), file);
420
+ console.log(
421
+ `\u001b[32m\u001b[1m[Funkophile]\u001b[0m Adding initial file: ${file}`
422
+ );
423
+ dispatchUpsert(
424
+ store,
425
+ inputRuleKey,
426
+ absoluteFilePath,
427
+ funkophileConfig.encodings
428
+ );
429
+ });
430
+
431
+ // Now set up the watcher
432
+ const watcher = chokidar
433
+ .watch(globPattern, {
434
+ cwd: process.cwd(),
435
+ ignoreInitial: true, // We've already processed initial files
436
+ persistent: true,
437
+ usePolling: false,
438
+ interval: 100,
439
+ binaryInterval: 300,
440
+ alwaysStat: false,
441
+ depth: 99,
442
+ awaitWriteFinish: {
443
+ stabilityThreshold: 50,
444
+ pollInterval: 10,
445
+ },
446
+ })
447
+ .on("error", (error) => {
448
+ console.error(
449
+ `\u001b[31m\u001b[1m[Funkophile]\u001b[0m Watcher error for pattern ${globPattern}:`,
450
+ error
451
+ );
452
+ logger.watchError(globPattern);
453
+ })
454
+ .on("add", (filePath) => {
455
+ console.log(
456
+ `\u001b[32m\u001b[1m[Funkophile]\u001b[0m File added: ${filePath}`
457
+ );
458
+ logger.watchAdd(filePath);
459
+ const absoluteFilePath = path.resolve(process.cwd(), filePath);
460
+ console.log(
461
+ `\u001b[32m\u001b[1m[Funkophile]\u001b[0m Dispatching UPSERT for key: ${inputRuleKey}, file: ${absoluteFilePath}`
462
+ );
463
+ dispatchUpsert(
464
+ store,
465
+ inputRuleKey,
466
+ absoluteFilePath,
467
+ funkophileConfig.encodings
468
+ );
469
+ })
470
+ .on("change", (filePath) => {
471
+ console.log(
472
+ `\u001b[33m\u001b[1m[Funkophile]\u001b[0m File changed: ${filePath}`
473
+ );
474
+ logger.watchChange(filePath);
475
+ const absoluteFilePath = path.resolve(process.cwd(), filePath);
476
+ console.log(
477
+ `\u001b[33m\u001b[1m[Funkophile]\u001b[0m Dispatching UPSERT for key: ${inputRuleKey}, file: ${absoluteFilePath}`
478
+ );
479
+ dispatchUpsert(
480
+ store,
481
+ inputRuleKey,
482
+ absoluteFilePath,
483
+ funkophileConfig.encodings
484
+ );
485
+ })
486
+ .on("unlink", (filePath) => {
487
+ console.log(
488
+ `\u001b[31m\u001b[1m[Funkophile]\u001b[0m File removed: ${filePath}`
489
+ );
490
+ logger.watchUnlink(filePath);
491
+ const absoluteFilePath = path.resolve(process.cwd(), filePath);
492
+ console.log(
493
+ `\u001b[31m\u001b[1m[Funkophile]\u001b[0m Dispatching REMOVE for key: ${inputRuleKey}, file: ${absoluteFilePath}`
494
+ );
495
+ store.dispatch({
496
+ type: REMOVE,
497
+ payload: {
498
+ key: inputRuleKey,
499
+ file: absoluteFilePath,
500
+ },
501
+ });
502
+ })
503
+ .on("unlinkDir", (filePath) => {
504
+ console.log(
505
+ `\u001b[31m\u001b[1m[Funkophile]\u001b[0m Directory removed: ${filePath}`
506
+ );
507
+ logger.watchUnlink(filePath);
508
+ })
509
+ .on("raw", (event, path, details) => {
510
+ console.log(
511
+ `\u001b[90m\u001b[1m[Funkophile]\u001b[0m Raw event: ${event} for path: ${path}`
512
+ );
513
+ });
514
+
515
+ console.log(
516
+ `\u001b[32m\u001b[1m[Funkophile]\u001b[0m Watcher is ready for pattern: ${globPattern}`
517
+ );
518
+ logger.watchReady(globPattern);
519
+ fulfill();
520
+ })
521
+ .catch((error) => {
522
+ console.error(
523
+ `\u001b[31m\u001b[1m[Funkophile]\u001b[0m Error processing initial files for pattern ${globPattern}:`,
524
+ error
525
+ );
526
+ reject(error);
527
+ });
528
+ // .on('raw', (event, p, details) => { // internal
529
+ // log('Raw event info:', event, p, details);
530
+ // })
531
+ } else {
532
+ console.error(
533
+ `mode should be 'watch' or 'build', not "${funkophileConfig.mode}"`
534
+ );
535
+ process.exit(-1);
536
+ }
537
+ });
538
+ });
539
+ }