Saving and Loading Files in a Web Page


This article is targeted at people who've started learning web programming. They'd made a few web pages with JavaScript. Maybe they've made a paint program using 2d canvas or a 3d scene using three.js. Maybe it's an audio sound maker, maybe it's a tile map editor. At some point they wonder "how do I save files"?

Maybe they have a save button that just puts all the data into a textarea and presents it to the user and says "copy this and paste it into notepad to save".

Well, the way you save in a web page is you save to a web server. OH THE HORROR! I hear you screaming WHAT? A server? Why would I want to install some giant server just to save data?

I'm here to show you a web server is not a giant piece of software. In fact it's tiny. The smallest web server in many languages can be written in a few lines of code.

For example node.js is a version of JavaScript that runs outside the browser. If you've ever used Perl or Python it works exactly the same. You install it. You give it files to run. It runs them. Perl you give perl files, python you give python files, node you give JavaScript files.

So using node.js here is the smallest web server

const http = require('http');
function handleRequest(request, response){
    response.end('Hello World: Path = ' + request.url);
http.createServer(handleRequest).listen(8000, function() { });


Now, all these 5 lines do is return "Hello World:: Path = <path>" for every page but really that's the basics of a web server. Looking at the code above without even explaining it you could imagine looking at request.url and deciding to do different things depending on what the url is. One URL might save, one might load, one might login, etc. That's really it.

Let's explain these 5 lines of code

const http = require('http');

require is the equivalent of import in python or #include in C++ or using in C# or in JavaScript using <script> src="..."></script>. It's loading the module 'http' and referencing it by the variable http.

function handleRequest(request, response){
    response.end('Hello World: Path = ' + request.url);

This is a function that will get called when we get a request from a browser. The request holds data about what the browser requested like the URL for the request and all the headers sent by the browser including cookies, what language the user's browser is set to, etc...

response is an object we can use to send our response back to the browser. As you can see here we're sending a string. We could also load a file and send the contents of that file. Or we would query a database and send back the results. But everything starts here.

const server = http.createServer(handleRequest);
server.listen(8000, function() { 
  console.log("Listening at http://localhost:8000");

The last line I expanded a little. First it calls http.createServer and passes it the function we want to be called for all requests.

Then it calls server.listen which starts it listening for requests from the browser. The 8000 is which port to listen on and the function is a callback to tell us when the server is up and running.


To run this server install node.js. Don't worry it's not some giant ass program. It's actually rather small. Much smaller than python or perl or any of those other languages.

Now open a terminal on OSX or on windows open a "Node Command Prompt" (node made this when you installed it).

Make a folder somewhere and cd to it in your terminal / command prompt

Make a file called index.js and copy and paste the 5 lines above into it. Save it.

Now type node index.js

In your browser open a new tab/window and go to http://localhost:8000 It should say Hello World: Path = \. If you type some other URL like http://localhost:8000/foo/bar?this=that you'll see it returns that back to you.

Congratulations, you just wrote a web server!

Let's add serving files

You can imagine the code to serve files. You'd parse the URL to get a path, read the corresponding file, call response.end(contentsOfFile). It's literally that easy. But, just to make it less code (and cover more cases) there's a library that does it for us and it's super easy to use.

Press Ctrl-C to stop your server if you haven't already. Then type

npm install express

It will download a bunch of files and put them in a subfolder called "node_modules". It will also probably print an error about no "package.json" which you can ignore (google package.json later)

Now let's edit our file again. We're going to replace the entire thing with this

"use strict";
const express = require('express');
const baseDir = 'public';

let app = express();
app.listen(8000, function() {
    console.log("listening at http://localhost:8000");

Looking at the last 2 lines you see app.listen(8000... just like before. That's because express is the same http server we had before. It's just added some structure we'll get to in a bit.

The cool part here is the line


It says "serve all the files from baseDir".

So, make a subfolder called "public". Inside make a file called test.html and inside that file put O'HI You. Save it. Now run your server again with node index.js

Go to http://localhost:8000/test.html in your browser. You should see "O'HI You" in your browser.

Congratulations. You have just made a web server that will serve any files you want all in 9 lines of code!

Let's Save Files

To save files we need to talk about HTTP methods. It's another piece of data the browser sends when it makes a request. Above we saw the browser sent the URL to the server. It also sends a method. The default method is called GET. There's nothing special about it. it's just a word. You can make up any words you want but there are 7 or 8 common ones and GET means "Get resource".

If you've ever made an XMLHttpRequest (and I hope you have because I'm not going to explain that part), you specify the method. Back on the server we could look at request.method to see what you specified and use that as yet another piece of data to decide what to do. If the method is GET we do one thing. If the method is BANANAS we do something else.

express has wrapped that http object from our first example and it adds a few major things.

(1) it does more parsing of request.url for us so we don't have to do it manually.

(2) it routes. Routing means we can tell it for any of various paths what function to call. For example we could say if the path starts with "/fruit" call the function HandleFruit and if the path starts with "/purchase/item/:itemnumber" then call HandleItemPurchase etc.. In our case we're going to just say we want all routes to call our function.

(3) it can route based on method. That way we don't have to check if the method was "GET" or "PUT" or "DELETE" or "BANANAS". We can just tell it to only call our handler if the path is XYZ and the method is ABC.

So let's update the code. Ctrl-C your server if you haven't already and edit index.js and update it to this

"use strict";
const express = require('express');
*const path = require('path');
*const fs = require('fs');
const baseDir = 'public';

let app = express();
*app.put('*', function(req, res) {
*    console.log("saving:", req.path);
*    let body = '';
*    req.on('data', function(data) { body += data; });
*    req.on('end', function() {
*        fs.writeFileSync(path.join(baseDir, req.path), body);
*        res.send('saved');
*    });
app.listen(8000, function() {
    console.log("listening at http://localhost:8000");

The first 2 added lines just reference more built in node libraries. path is a library for manipulating file paths. fs stands for "file system" and is a library for dealing with files.

Next we call app.put which takes 2 arguments. The first is the route and '*' just means "all routes". Then it takes a function to call for this route. app.put only routes "PUT" method requests so this line effectively says "Call our function for every route when the method is "PUT".

The function adds a tiny event handler to the data event that reads the data the browser is sending by adding it to a string called body. It adds another tiny event handler to the end event that then writes out the data to a file and sends back the message 'saved'.

And that's it! We'd made a server that saves and loads files. It's very insecure because it can save and load any file but if we're only using it for local stuff it's a great start.

Loading And Saving From the Browser

The final thing to do is to test it out by writing the browser side. I'm going to assume if you've already made some web pages and you're at the point where you want to load and save that you probably have some idea of what XMLHttpRequest is and how to make forms and check for users clicking on buttons etc. So with that in mind here's the new test.html

    textarea {
        display: block;

<label for="savefilename">filename:</label>
<input id="savefilename" type="text" value="myfile.txt" />
<textarea id="savedata">
this is some test data
<button id="save">Save</button>

<label for="loadfilename">filename:</label>
<input id="loadfilename" type="text" value="myfile.txt" />
<textarea id="loaddata">
<button id="load">Load</button>

// make $ a shortcut for document.querySelector
const $ = document.querySelector.bind(document);

// when the user clicks 'save'
$("#save").addEventListener('click', function() {

    // get the filename and data
    const filename = $("#savefilename").value;
    const data = $("#savedata").value;

    // save
    saveFile(filename, data, function(err) {
        if (err) {
            alert("failed to save: " + filename + "\n" + err);
        } else {
            alert("saved: " + filename);

// when the user clicks load
$("#load").addEventListener('click', function() {

    // get the filename
    const filename = $("#loadfilename").value;

    // load 
    loadFile(filename, function(err, data) {
        if (err) {
            alert("failed to load: " + filename + "\n" + err);
        } else {
            $("#loaddata").value = data;
            alert("loaded: " + filename);

function saveFile(filename, data, callback) {
    doXhr(filename, 'PUT', data, callback);

function loadFile(filename, callback) {
    doXhr(filename, 'GET', '', callback);

function doXhr(url, method, data, callback) {
  const xhr = new XMLHttpRequest();, url);
  xhr.onload = function() {
      if (xhr.status === 200) {
          callback(null, xhr.responseText);
      }  else {
          callback('Request failed.  Returned status of ' + xhr.status);

If you now save that and run your server then go to http://localhost:8000/test.html you should be able to type some text in the savedata area and click "save". Afterwords click "load" and you'll see it got loaded. Check your hard drive and you'll see a file has been created.

Now of course again this is insecure. For example if you type in "text.html" in the save filename and pick "save" it's going to overwrite your "text.html" file. Maybe you should pick a different route instead of "*" in the app.put('*', ... line. Maybe you want to add a check to see if the file exists with another kind of method and only update if the user is really sure.

The point of this article was not to make a working server. It was to show you how easy making a server is. A server like this that saves local files is probably only useful for things like an internal tool you happened to write in JavaScript that you and/or your team needs. They could run a small server like this on their personal machines and have your tool load, save, get folder listings, etc.

But, seeing how easy it is also hopefully demystifies servers a little. You can start here and then graduate to whole frameworks that let users login and share files remotely.

I should also mention you can load and save files to things like Google Drive or Dropbox. Now you know what they're basically doing behind the scenes.


CAs now get to decide who's on the Internet


It started with a legit concern. The majority of websites were served using HTTP. HTTP is insecure. So what you might be thinking? HTTPS is used on my bank and amazon and anywhere I might spend money so it seems not a problem. Except ... HTTP allows injections. Ever use some bad hotel or bad airport WiFi and get a banner injected at the top of the screen? That's HTTP vs HTTPS. Are you sure those articles you're reading are the originals? Maybe someone is changing words, pictures, or ads. HTTPS solves these issues.

So, the browser vendors and other standards bodies got together and made a big push for HTTPS only. Sounds great right!?

Well instead of just pushing metaphorically by putting out the word, "Stop using HTTP! Start using HTTPS" the browser vendors got together and decided to try to kill off HTTP completely. Their first order of business was to start requiring HTTPS to use certain features in the browser. Want to go fullscreen? Your site must be served from HTTPS. Want to read the orientation and motion data of the phone from the browser? Your website must use HTTPS. Want to be able to ask the user for permission to access to mic or the camera? Your website must use HTTPS.

Okay well that certainly can be motivating to switch to HTTPS as soon as possible.

Except ... HTTPS requires certificates. Those certificates can only be acquired from Certificate Authorities. CAs for short. CAs charge money for these certificates. $50 per certificate or more. Often the certificates only last for a limited time so you've got to pay every year or 2.

Suddenly every website just got more expensive.

Okay you say but that's still not a ton of money.

Yes but, maybe you've got an innovative project. One that lets any user access their media from their browser(example). You'd like to let them go fullscreen but you can't unless it serves the media pages as HTTPS. The rules of HTTPS say your not allowed to share certs ever. If you get caught sharing your cert will be invalidated. So, you can't give each of these people running your innovated software a copy of your cert. Instead every user needs their own cert. Suddenly your software just got a lot more expensive! What if your software was free and open source? In 2015 people were able to run it for free. In 2016 the are now required to get a cert for $50

So what do you do? Well you hear about self−signed certs. So you check those out. Turns out they require complex installation into your OS. Your family and aunts and uncles and cousins and nephews and nieces aren't going to find that really manageable. And besides there's the feature where anyone can come to a party at your places and queue some music videos using their phone's browser but that's never going to fly if they have to first install this self−signed cert. Official certs from CAs don't have this issue. They just work.

Okay well you shop around for CA's. Dear CA#1 will you give my users free certs? No! Dear CA#2 will you give my users free certs? No!

Oh I hear you say, there's a new kid on the block, letsencrypt, they offer free certs.

They do offer free certs BUT, certs are tied to domain names. To get a cert from letsencrpyt you have to have a domain. Example "". So even if you can get the cert for free your users now need to buy a domain name. That can be relatively cheap at $10−$20 a year but it's a big technical hurdle. Your non−tech family members are not really going to be able to go through all the process of getting a domain name just to use your media server.

Oh I hear you say, what if my software ran a public DNS server. I could issue users subdomains like "<username>". Then I can give out DNS names to the users and they can get certs. That might work ... except, DNS points to specific IP addresses. User's IP address changes. You can re−point DNS to the new address but it takes time to propagate. That means when their IP address changes it might be a few hours until they can access their media again. Not going to work.

Ok then here's a solution. We'll make up domains like this "<ipaddress>.<username>". That will make the DNS server even easier. We don't even need a database. We just look at the "<ipaddress>" part of the DNS name and return that IP address. Now when the user's IP address changes there will be zero delay because they can immediately use a DNS name that matches. We'll setup some rendezvous server for them so they don't need to lookup the correct domain. It will all just work.

Great! We have domains. We can get free certs from letsencrypt.

Except....letsencrypt limits the number of certs to 240 per root domain. So once you have 240 you can't get more certs. That means we can only support 240 users at best. But then there's another problem. Letsencrpt doesn't support wildcard certs. Because we added the part above we need a wildcard cert for each user matching "*.<username>".

Effectively we are S.O.L. For our purposes letsencrypt is just another CA. "CA#3 can we please have free certs for our users?" No!

As of 2015 we could do anything we wanted on the internet. Now in 2016 we need permission from a CA. If the CA doesn't give permission we don't get on the internet.

To put it another way because of the chain of validation in HTTPS each CA is effectively a little king/bureaucrat who gets to decide who gets on the internet and who doesn't. If one king doesn't let you your only option is to go ask another king. Letsencyrpt is the most generous king as they don't ask for tribute but that doesn't change the fact you still need permission from one of these kings.

You might be thinking, "so what? who cares about a media streamer?". But it's not just streamers. It's "ANY DEVICE OR SOFTWARE THAT SERVES A WEBPAGE". Got an IP camera that serves a webpage? That camera wants to give you a nice interface that goes fullscreen? It can't without certs and it can't get certs without permission from a CA. Got some Raspberry PI project that wants to serve a webpage and needs any of the banned features? Again, it can't do it without a cert and it can't get a cert without permission from a CA. Maybe you have a NAS device and it would like provide web page access? It can't do it without a cert and it can't get certs without permission from a CA.

That wasn't the case just 6 months ago because HTTPS wasn't required. Now that it is these kings all just got a bunch more power and innovative products like the media streamer described above and projects like this are effectively discouraged unless you can beg or bribe a king to ordain them. 😎




Sorry to rant on this more but sheesh! How much does it take?

So I want to watch the filesystem for changes. Unfortunately node's isn't OS independant. Some OSes return individual files. Other OSes just return the parent folder changed. I'm sure there's some legit reason like perf not to fix this for all devs but whatever. I assume there must be a library that fixes it.

So I search NPM. Maybe I should be using Google to search NPM because NPMs results are usually pretty poor but after searching a while I find this watch module. It's got nearly 700k downloads in the last month and tons of dependents so it must be good.

Given my prior experience the first thing I check is how many dependencies does it list?

Okay only 2 but they are both related to command line stuff. WTF does a library have command line dependencies? Oh, I see this library is conflated with a utility that watches for changes and launches a specified program anytime a file changes. Sigh... ?

Okay well let's look through the dependencies. It's not too deep. Fine, maybe I'll live with it if it works.

Ok I file an issue "Separate library from command line?" and move on.

A quick glance at the code and it doesn't appear to abstract the issues I need fixed but it's easy to test. So I write some test code

var watch = require('watch');

var dir = process.argv[2];
console.log("watching: ", dir);

var options = {
  ignoreDotFiles: true, // - When true this option means that when the file tree is walked it will ignore files that being with "."
  filter: function() { return true; }, // - You can use this option to provide a function that returns true or false for each file and directory to decide whether or not that file/directory is included in the watcher.
  ignoreUnreadableDir: true, // - When true, this options means that when a file can't be read, this file is silently skipped.
  ignoreNotPermitted: true, // - When true, this options means that when a file can't be read due to permission issues, this file is silently skipped.
  ignoreDirectoryPattern: /node_modules/, // - When a regex pattern is set, e.g. /node_modules/, these directories are silently skipped.

watch.createMonitor(dir, options, function(monitor) {
  monitor.on('created', function(f, s)     { show("created", f, s    ); });
  monitor.on('removed', function(f, s, s2) { show("removed", f, s, s2); });
  monitor.on('changed', function(f, s)     { show("changed", f, s    ); });

function show(event, f, s, n) {
  console.log(event, f);

I run it and pass it my temp folder which has a ton of crap in it including lots of sub folders.

I edit temp/foo.txt and see it print changed: temp/foo.txt. I delete temp/foo.txt and see it print removed: temp/foo.txt. I create a temp/foo.txt and see it print created: temp/foo.txt.

So far so good. Let's test one more thing.

I look for a subfolder since I've only been changing stuff in the root of the folder I'm watching. I just happen to pick temp/delme-eslint/node_modules/foo. BOOM! It prints

created temp/delme-eslint/node_modules/abab
created temp/delme-eslint/node_modules/abbrev
created temp/delme-eslint/node_modules/acorn
created temp/delme-eslint/node_modules/acorn-globals
created temp/delme-eslint/node_modules/acorn-jsx
created temp/delme-eslint/node_modules/amdefine
created temp/delme-eslint/node_modules/ansi-escapes
created temp/delme-eslint/node_modules/ansi-regex
created temp/delme-eslint/node_modules/ansi-styles
created temp/delme-eslint/node_modules/argparse
created temp/delme-eslint/node_modules/array-differ
created temp/delme-eslint/node_modules/array-union
created temp/delme-eslint/node_modules/array-uniq
created temp/delme-eslint/node_modules/arrify
created temp/delme-eslint/node_modules/asn1
created temp/delme-eslint/node_modules/assert-plus
created temp/delme-eslint/node_modules/async
created temp/delme-eslint/node_modules/aws-sign2
created temp/delme-eslint/node_modules/aws4
created temp/delme-eslint/node_modules/balanced-match
created temp/delme-eslint/node_modules/bl
created temp/delme-eslint/node_modules/bluebird
created temp/delme-eslint/node_modules/boolbase
created temp/delme-eslint/node_modules/boom
created temp/delme-eslint/node_modules/brace-expansion
created temp/delme-eslint/node_modules/caller-path
created temp/delme-eslint/node_modules/callsites
created temp/delme-eslint/node_modules/caseless
created temp/delme-eslint/node_modules/chalk
created temp/delme-eslint/node_modules/cheerio
created temp/delme-eslint/node_modules/cli-cursor
created temp/delme-eslint/node_modules/cli-width
created temp/delme-eslint/node_modules/code-point-at
created temp/delme-eslint/node_modules/coffee-script
created temp/delme-eslint/node_modules/color-convert
created temp/delme-eslint/node_modules/colors
created temp/delme-eslint/node_modules/combined-stream
created temp/delme-eslint/node_modules/commander
created temp/delme-eslint/node_modules/concat-map
created temp/delme-eslint/node_modules/concat-stream
created temp/delme-eslint/node_modules/core-util-is
created temp/delme-eslint/node_modules/cryptiles
created temp/delme-eslint/node_modules/css-select
created temp/delme-eslint/node_modules/css-what
created temp/delme-eslint/node_modules/cssom
created temp/delme-eslint/node_modules/cssstyle
created temp/delme-eslint/node_modules/d
created temp/delme-eslint/node_modules/dashdash
created temp/delme-eslint/node_modules/dateformat
created temp/delme-eslint/node_modules/debug
created temp/delme-eslint/node_modules/deep-is
created temp/delme-eslint/node_modules/del
created temp/delme-eslint/node_modules/delayed-stream
created temp/delme-eslint/node_modules/doctrine
created temp/delme-eslint/node_modules/dom-serializer
created temp/delme-eslint/node_modules/domelementtype
created temp/delme-eslint/node_modules/domhandler
created temp/delme-eslint/node_modules/domutils
created temp/delme-eslint/node_modules/ecc-jsbn
created temp/delme-eslint/node_modules/entities
created temp/delme-eslint/node_modules/es5-ext
created temp/delme-eslint/node_modules/es6-iterator
created temp/delme-eslint/node_modules/es6-map
created temp/delme-eslint/node_modules/es6-set
created temp/delme-eslint/node_modules/es6-symbol
created temp/delme-eslint/node_modules/es6-weak-map
created temp/delme-eslint/node_modules/escape-string-regexp
created temp/delme-eslint/node_modules/escodegen
created temp/delme-eslint/node_modules/escope
created temp/delme-eslint/node_modules/eslint
created temp/delme-eslint/node_modules/espree
created temp/delme-eslint/node_modules/esprima
created temp/delme-eslint/node_modules/esrecurse
created temp/delme-eslint/node_modules/estraverse
created temp/delme-eslint/node_modules/esutils
created temp/delme-eslint/node_modules/event-emitter
created temp/delme-eslint/node_modules/eventemitter2
created temp/delme-eslint/node_modules/exit
created temp/delme-eslint/node_modules/exit-hook
created temp/delme-eslint/node_modules/extend
created temp/delme-eslint/node_modules/extsprintf
created temp/delme-eslint/node_modules/fast-levenshtein
created temp/delme-eslint/node_modules/figures
created temp/delme-eslint/node_modules/file-entry-cache
created temp/delme-eslint/node_modules/find-up
created temp/delme-eslint/node_modules/findup-sync
created temp/delme-eslint/node_modules/flat-cache
created temp/delme-eslint/node_modules/foo
created temp/delme-eslint/node_modules/forever-agent
created temp/delme-eslint/node_modules/form-data
created temp/delme-eslint/node_modules/generate-function
created temp/delme-eslint/node_modules/generate-object-property
created temp/delme-eslint/node_modules/getobject
created temp/delme-eslint/node_modules/glob
created temp/delme-eslint/node_modules/globals
created temp/delme-eslint/node_modules/globby
created temp/delme-eslint/node_modules/graceful-fs
created temp/delme-eslint/node_modules/graceful-readlink
created temp/delme-eslint/node_modules/grunt
created temp/delme-eslint/node_modules/grunt-eslint
created temp/delme-eslint/node_modules/grunt-legacy-log
created temp/delme-eslint/node_modules/grunt-legacy-log-utils
created temp/delme-eslint/node_modules/grunt-legacy-util
created temp/delme-eslint/node_modules/har-validator
created temp/delme-eslint/node_modules/has-ansi
created temp/delme-eslint/node_modules/hoek
created temp/delme-eslint/node_modules/hawk
created temp/delme-eslint/node_modules/hooker
created temp/delme-eslint/node_modules/htmlparser2
created temp/delme-eslint/node_modules/http-signature
created temp/delme-eslint/node_modules/iconv-lite
created temp/delme-eslint/node_modules/ignore
created temp/delme-eslint/node_modules/inflight
created temp/delme-eslint/node_modules/inherits
created temp/delme-eslint/node_modules/inquirer
created temp/delme-eslint/node_modules/is-fullwidth-code-point
created temp/delme-eslint/node_modules/is-my-json-valid
created temp/delme-eslint/node_modules/is-path-cwd
created temp/delme-eslint/node_modules/is-path-in-cwd
created temp/delme-eslint/node_modules/is-path-inside
created temp/delme-eslint/node_modules/is-property
created temp/delme-eslint/node_modules/is-resolvable
created temp/delme-eslint/node_modules/is-typedarray
created temp/delme-eslint/node_modules/isarray
created temp/delme-eslint/node_modules/isstream
created temp/delme-eslint/node_modules/jodid25519
created temp/delme-eslint/node_modules/js-yaml
created temp/delme-eslint/node_modules/jsbn
created temp/delme-eslint/node_modules/jsdom
created temp/delme-eslint/node_modules/json-schema
created temp/delme-eslint/node_modules/json-stable-stringify
created temp/delme-eslint/node_modules/json-stringify-safe
created temp/delme-eslint/node_modules/jsonify
created temp/delme-eslint/node_modules/jsonpointer
created temp/delme-eslint/node_modules/jsprim
created temp/delme-eslint/node_modules/levn
created temp/delme-eslint/node_modules/load-grunt-tasks
created temp/delme-eslint/node_modules/lodash
created temp/delme-eslint/node_modules/lru-cache
created temp/delme-eslint/node_modules/mime-db
created temp/delme-eslint/node_modules/mime-types
created temp/delme-eslint/node_modules/minimatch
created temp/delme-eslint/node_modules/minimist
created temp/delme-eslint/node_modules/mkdirp
created temp/delme-eslint/node_modules/ms
created temp/delme-eslint/node_modules/multimatch
created temp/delme-eslint/node_modules/mute-stream
created temp/delme-eslint/node_modules/node-uuid
created temp/delme-eslint/node_modules/nopt
created temp/delme-eslint/node_modules/nth-check
created temp/delme-eslint/node_modules/number-is-nan
created temp/delme-eslint/node_modules/nwmatcher
created temp/delme-eslint/node_modules/oauth-sign
created temp/delme-eslint/node_modules/object-assign
created temp/delme-eslint/node_modules/once
created temp/delme-eslint/node_modules/onetime
created temp/delme-eslint/node_modules/optionator
created temp/delme-eslint/node_modules/os-homedir
created temp/delme-eslint/node_modules/parse5
created temp/delme-eslint/node_modules/path-exists
created temp/delme-eslint/node_modules/path-is-absolute
created temp/delme-eslint/node_modules/path-is-inside
created temp/delme-eslint/node_modules/pify
created temp/delme-eslint/node_modules/pinkie
created temp/delme-eslint/node_modules/pinkie-promise
created temp/delme-eslint/node_modules/pkg-up
created temp/delme-eslint/node_modules/pluralize
created temp/delme-eslint/node_modules/prelude-ls
created temp/delme-eslint/node_modules/process-nextick-args
created temp/delme-eslint/node_modules/progress
created temp/delme-eslint/node_modules/pseudomap
created temp/delme-eslint/node_modules/qs
created temp/delme-eslint/node_modules/read-json-sync
created temp/delme-eslint/node_modules/readable-stream
created temp/delme-eslint/node_modules/readline2
created temp/delme-eslint/node_modules/request
created temp/delme-eslint/node_modules/require-uncached
created temp/delme-eslint/node_modules/resolve
created temp/delme-eslint/node_modules/resolve-from
created temp/delme-eslint/node_modules/resolve-pkg
created temp/delme-eslint/node_modules/restore-cursor
created temp/delme-eslint/node_modules/rimraf
created temp/delme-eslint/node_modules/run-async
created temp/delme-eslint/node_modules/rx-lite
created temp/delme-eslint/node_modules/sax
created temp/delme-eslint/node_modules/shelljs
created temp/delme-eslint/node_modules/sigmund
created temp/delme-eslint/node_modules/slice-ansi
created temp/delme-eslint/node_modules/sntp
created temp/delme-eslint/node_modules/source-map
created temp/delme-eslint/node_modules/sprintf-js
created temp/delme-eslint/node_modules/sshpk
created temp/delme-eslint/node_modules/string-width
created temp/delme-eslint/node_modules/string_decoder
created temp/delme-eslint/node_modules/stringstream
created temp/delme-eslint/node_modules/strip-ansi
created temp/delme-eslint/node_modules/strip-json-comments
created temp/delme-eslint/node_modules/supports-color
created temp/delme-eslint/node_modules/symbol-tree
created temp/delme-eslint/node_modules/table
created temp/delme-eslint/node_modules/text-table
created temp/delme-eslint/node_modules/through
created temp/delme-eslint/node_modules/tough-cookie
created temp/delme-eslint/node_modules/tr46
created temp/delme-eslint/node_modules/tryit
created temp/delme-eslint/node_modules/tunnel-agent
created temp/delme-eslint/node_modules/tv4
created temp/delme-eslint/node_modules/tweetnacl
created temp/delme-eslint/node_modules/type-check
created temp/delme-eslint/node_modules/typedarray
created temp/delme-eslint/node_modules/underscore
created temp/delme-eslint/node_modules/underscore.string
created temp/delme-eslint/node_modules/user-home
created temp/delme-eslint/node_modules/util-deprecate
created temp/delme-eslint/node_modules/verror
created temp/delme-eslint/node_modules/webidl-conversions
created temp/delme-eslint/node_modules/whatwg-url-compat
created temp/delme-eslint/node_modules/which
created temp/delme-eslint/node_modules/wordwrap
created temp/delme-eslint/node_modules/wrappy
created temp/delme-eslint/node_modules/write
created temp/delme-eslint/node_modules/xml-name-validator
created temp/delme-eslint/node_modules/xregexp
created temp/delme-eslint/node_modules/xtend
created temp/delme-eslint/node_modules/yallist

BUG!!! Really, I used it for 5 minutes ran into bug and yet 700k downloads and a ton of dependents are using this buggy library. Sigh....?

Now the question is (a) fix it or 🍺 write my own.

Yes I know there are good arguments for fixing it. Just last month 700k downloads = 700k fixed downloads next month probably. But of course it possible means arguing with the author, jumping through their hoops. And, I still don't know if it solves the problem I want solved.

On top of that I there's at least one thing I wanted to re−design I think. Not only do I want the tree to be watched, I want to know what's in the tree. It kind of seems like that should be one operation. In other words I don't want to call some other function to scan the tree to get the list of all things in it and then separately call another function to watch the tree. If I do that then a file that's created between the first scan and the watch will never show up. It will also be twice as slow AFAICT as the the watch has to scan as well. So, now yet another discussion to be haggled over if I decide to fix vs do it myself.

The worst part is the thing I wanted to do I thought would take all of 20 minutes if that functionality I was looking for existed. Now will also probably take 8x as long to fix vs do it myself on top of writing the feature I was originally setting out to d. Probably not fair but (a) no negociation and 🍺 I feel more obligated to right tests for fixing but not for my own code ?



I filed a bug on that issue but I didn't purse fixing it. Instead I wrote my own. It took about 20hrs of solid work. Probably only a 2 hours to write but 18 hours to test across 3 platforms and learn new issues. I found out ubuntu, osx, windows, and travis−ci's containers all behave differently in this area. I'm sure I didn't catch all the stuff and I still would like to write another 4−8 hours of tests but I need to move on and it's working so far.

Some things that were interesting is where to make the divisions, what features to put in. For example the most used library I linked to above, the buggy one, has options for ignoring dot files (files that start with .) as well as an option to give it a regular expression to filter (the one that didn't actually work). After writing mine I found another library that takes complex globs and has debouncing features.

I feel like those features should be layers. It's probably as little as one line of code to filter. Example:

  var watcher = new SimpleTreeWatcher(pathToWatch, {
    filter: (filepath) => { return path.basename(filepath)[0] !== '.'); },

So why clutter the library with options when doing it this way keeps the library simple and let's people be as complex or simple as they want. It seems better to just provide a few examples of how to filter than have a bunch of options.

Similarly the debouncing can relatively easily be layered on top. I should add examples to the docs but at least I have something that's working for me so far.


Crunch in Games


So this commentary on some other commentary is making the rounds.−inline−response−to−wage−slaves/

Let me first make it clear I'm 100% on the side of less crunch. You'll probably forget that after reading my comments but, at least try to remember.

So I agree with Rami's commentary I just wanted to call out a few things


Dynamic Typing > Static Typing?


I stumbled upon this amazing video called "The Unreasonable Effectiveness of Dynamic Typing for Practical Programs"

It's about 50mins long and unfortunately vimeo has no fast playback options like youtube so I'd suggest you download it and view it in VLC then click Ctrl+ or Cmd+ to speed up

In any case he makes a very compelling argument that static typing isn't really all that. His argument is LOOK AT THE DATA!!! Everyone has opinions. People believe static typing catches bugs. People believe static typing helps document code. People believe static typing makes IDEs work better and therefore save time, etc. BUT ... those are all just beliefs not backed up by any data. He points out that so far all the studies suggest that's not true. He claims 5 years ago he was a static type believer but after looking up the research and doing some of his own he's convinced static typing is a net negative.

You should really watch the talk but I'll try to sum it up here


Stack Overflow Attribution Required


So Stack Overflow apparently requires attribution?!??

This has apparently always been the case but it came up recently because they were planning to change the license.

This made me wonder, why? Why should I attribute SO? I certainly don't want attribution for all my time spend on Stack Overflow. Most of the stuff I look up on Stack Overflow is just the same stuff I could find on MDN or MSDN or some reference manual for some library somewhere but it's more work to read the entire manual front to back to grok it than it is to just google it and end up getting pointed to the answer on SO.


Writing Good Documentation and Examples


I was going to start this as a rant for how bad most docs are but instead I'll just list what I think are some best practices. I know no one will read this but what else can I do 😉


More JavaScript vs C#/C++


I ranted before on JavaScript vs C#/C++ and for some reason I was in the mood to revisit that.

Basically the last 2 years I've been mostly in JavaScript land. Like most C++ programmers I hated it at first. But now I find it hard to have any desire to switch back. Of course I'll program in whatever language I need to to do the job. C++ if I'm Unreal. C# if I'm in Unity.

But, kind of like J−Blow mentioned in his first few videos it's frustrating every time there is busy work to do in C++ or C# that there is not when in JavaScript. To be clear he only mentioned his frustration with C++. AFAICT he has no serious experience with JavaScript. I expect he's at stage #1 or stage #2 at best




As fun as it is to make vertexshaderart I'm also tweaking the site

So for example, while this is probably a bad idea as it might end up costing me more money I cleaned things up a little so you embedding is nicer

This one uses the mouse. It's based on art by paweł which is linked to below.

Figuring out how to get a good response in the visuals so far as been just a guessing game. This one mostly seemed to work once the song really gets started.

This one too

And just trying new stuff

You can see a particular person's creations for example P_Malin has made some pretty great pieces.

For a given piece you can also see the revisions

And you can mark pieces as private if you want to work on them before letting others see.

I'm trying to figure out how to get more variety on the main page. Originally I had "popular" which was just by likes but the same few things were always at the top. I might do something like aging things so the older they are the more likes or views they need to stay at the top but at the same time I don't want to see the main page covered with uninteresting stuff like some of mine so I'm not sure what to do there. If you have ideas comment below.



Your challenge should you decide to accept it: Given only a count that goes from 0 to N generate vertices and colors