summaryrefslogtreecommitdiff
path: root/config/helpers/incremental_webpack_compiler.js
blob: 786bb6071fa3b966f4885707877329672df92413 (plain)
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
const fs = require('fs');
const path = require('path');

const log = (msg, ...rest) => console.log(`IncrementalWebpackCompiler: ${msg}`, ...rest);

// If we force a recompile immediately, the page reload doesn't seem to work.
// Five seconds seem to work fine and the user can read the message
const TIMEOUT = 5000;

class NoopCompiler {
  constructor() {
    this.enabled = false;
  }

  filterEntryPoints(entryPoints) {
    return entryPoints;
  }

  logStatus() {}

  setupMiddleware() {}
}

class IncrementalWebpackCompiler {
  constructor(historyFilePath) {
    this.enabled = true;
    this.history = {};
    this.compiledEntryPoints = new Set([
      // Login page
      'pages.sessions.new',
      // Explore page
      'pages.root',
    ]);
    this.historyFilePath = historyFilePath;
    this._loadFromHistory();
  }

  filterEntryPoints(entrypoints) {
    return Object.fromEntries(
      Object.entries(entrypoints).map(([key, val]) => {
        if (this.compiledEntryPoints.has(key)) {
          return [key, val];
        }
        return [key, ['./webpack_non_compiled_placeholder.js']];
      }),
    );
  }

  logStatus(totalCount) {
    const current = this.compiledEntryPoints.size;
    log(`Currently compiling route entrypoints: ${current} of ${totalCount}`);
  }

  setupMiddleware(app, server) {
    app.use((req, res, next) => {
      const fileName = path.basename(req.url);

      /**
       * We are only interested in files that have a name like `pages.foo.bar.chunk.js`
       * because those are the ones corresponding to our entry points.
       *
       * This filters out hot update files that are for example named "pages.foo.bar.[hash].hot-update.js"
       */
      if (fileName.startsWith('pages.') && fileName.endsWith('.chunk.js')) {
        const chunk = fileName.replace(/\.chunk\.js$/, '');

        this._addToHistory(chunk);

        if (!this.compiledEntryPoints.has(chunk)) {
          log(`First time we are seeing ${chunk}. Adding to compilation.`);

          this.compiledEntryPoints.add(chunk);

          setTimeout(() => {
            server.middleware.invalidate(() => {
              if (server.sockets) {
                server.sockWrite(server.sockets, 'content-changed');
              }
            });
          }, TIMEOUT);
        }
      }

      next();
    });
  }

  // private methods

  _addToHistory(chunk) {
    if (!this.history[chunk]) {
      this.history[chunk] = { lastVisit: null, count: 0 };
    }
    this.history[chunk].lastVisit = Date.now();
    this.history[chunk].count += 1;

    try {
      fs.writeFileSync(this.historyFilePath, JSON.stringify(this.history), 'utf8');
    } catch (e) {
      log('Warning – Could not write to history', e.message);
    }
  }

  _loadFromHistory() {
    try {
      this.history = JSON.parse(fs.readFileSync(this.historyFilePath, 'utf8'));
      const entryPoints = Object.keys(this.history);
      log(`Successfully loaded history containing ${entryPoints.length} entry points`);
      /*
      TODO: Let's ask a few folks to give us their history file after a milestone of usage
            Then we can make smarter decisions on when to throw out rather than rendering everything
            Something like top 20/30/40 entries visited in the last 7/10/15 days might be sufficient
       */
      this.compiledEntryPoints = new Set([...this.compiledEntryPoints, ...entryPoints]);
    } catch (e) {
      log(`No history found...`);
    }
  }
}

module.exports = (enabled, historyFilePath) => {
  log(`Status – ${enabled ? 'enabled' : 'disabled'}`);

  if (enabled) {
    return new IncrementalWebpackCompiler(historyFilePath);
  }
  return new NoopCompiler();
};