Skip to content

Commit

Permalink
Add initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
rtsao committed Jun 6, 2018
1 parent 099a809 commit db48893
Show file tree
Hide file tree
Showing 19 changed files with 6,029 additions and 0 deletions.
32 changes: 32 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
module.exports = {
parser: "babel-eslint",
parserOptions: {
ecmaVersion: 2017,
},
env: {
es6: true
},

extends: [
"eslint:recommended"
],

plugins: ["eslint-plugin-prettier"],

rules: {
"prettier/prettier": [
"error",
{
useTabs: false,
printWidth: 80,
tabWidth: 2,
singleQuote: false,
trailingComma: "all",
bracketSpacing: false,
jsxBracketSameLine: false,
parser: "babylon",
semi: true
}
]
}
};
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules/
worker/worker.js
fixtures/app/static
yarn-error.log
15 changes: 15 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
language: node_js
node_js:
- "8"
sudo: false
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.7.0
- export PATH=$HOME/.yarn/bin:$PATH
before_script:
- yarn workspace css-to-js-sourcemap-worker run prepare
- yarn workspace css-to-js-sourcemap-fixture-app run prepare
script:
- yarn run lint
- yarn run test
cache:
yarn: true
208 changes: 208 additions & 0 deletions core/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/* eslint-env worker */

import {SourceMapConsumer} from "source-map/lib/source-map-consumer";
import ErrorStackParser from "error-stack-parser";
import SourceMapUrl from "source-map-url";
import {encode} from "sourcemap-codec";

class Token {
constructor() {
this.cancelled = false;
}
cancel() {
this.cancelled = true;
}
}

let invalidationToken = new Token();

function task(fn) {
const token = invalidationToken;
return result => {
if (!token.cancelled) {
return fn(result);
}
};
}

const state = {
mapperCache: new Map(),
sourceCache: new Map(),
inboundRequests: new Set(),
// List of mapped class names for batch rendering
renderQueue: [],
};

export function initWasm(url) {
SourceMapConsumer.initialize({
"lib/mappings.wasm": url,
});
}

export function renderCSS() {
if (state.renderQueue.length === 0) {
return "";
}
const {rules, segments, sources} = state.renderQueue.reduce(
(acc, {className, line, source}) => {
let sourceIndex = acc.sources.indexOf(source);
if (sourceIndex === -1) {
sourceIndex = acc.sources.push(source) - 1;
}
acc.rules.push(`.${className} {}`);
acc.segments.push([[0, sourceIndex, line - 1, 0]]);
return acc;
},
{rules: [], segments: [], sources: []},
);
state.renderQueue = [];
const mappings = encode(segments);

const map = {
version: 3,
sources,
mappings,
sourcesContent: sources.map(source => state.sourceCache.get(source)),
};
const json = JSON.stringify(map);
const base64 = btoa(json);

const comment = `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${base64} */`;
return `${rules.join("\n")}\n${comment}`;
}

export function addMappedClass({error, stackIndex, className}) {
addMappedClassAsync({error, stackIndex, className});
}

export function invalidate() {
// Token should be immediately invalidated
invalidationToken.cancel();
invalidationToken = new Token();

// After invalidation, existing mapped class names should be rendered
// using the existing sourceCache
const css = renderCSS();

state.mapperCache = new Map();
state.sourceCache = new Map();

// Replay inbound requests with cleared caches
for (const request of state.inboundRequests) {
addMappedClassAsync(request);
}

return css;
}

function addMappedClassAsync(request) {
state.inboundRequests.add(request);
const {error, stackIndex, className} = request;
const location = getLocation(error, stackIndex);
return getMapper(location.filename)
.then(
task(mapper => {
const mapped = mapper.originalPositionFor(location);
if (!state.sourceCache.has(mapped.source)) {
state.sourceCache.set(
mapped.source,
mapper.sourceContentFor(mapped.source),
);
}
state.renderQueue.push({
className,
source: mapped.source,
line: mapped.line,
column: mapped.column,
});
state.inboundRequests.remove(request);
}),
)
.catch(err => {
// eslint-disable-next-line no-console
console.warn("Debug worker error", err);
});
}

function getIdentityMapper(sourceName, sourceContents) {
return {
originalPositionFor: ({line, column}) => ({
line,
column,
source: sourceName,
}),
sourceContentFor: () => {
return sourceContents;
},
};
}

async function getMapper(filename) {
const cached = state.mapperCache.get(filename);
if (cached) {
return cached;
}

const result = fetch(filename)
.then(
task(res => {
return res.text();
}),
)
.then(
task(src => {
const url = SourceMapUrl.getFrom(src);
return url ? getMapperFromUrl(url) : getIdentityMapper(filename, src);
}),
);

state.mapperCache.set(filename, result);
return result;
}

function getMapperFromUrl(url) {
return getSourceMapJsonFromUrl(url).then(
task(map => {
return new SourceMapConsumer(map);
}),
);
}

function getLocation(error, stackIndex) {
const frame = ErrorStackParser.parse(error)[stackIndex];
if (!frame.fileName) {
throw new Error("Could not locate file");
}
return {
filename: frame.fileName,
line: frame.lineNumber,
column: frame.columnNumber,
};
}

async function getSourceMapJsonFromUrl(url) {
return isDataUrl(url)
? parseDataUrl(url)
: fetch(url).then(
task(res => {
return res.json();
}),
);
}

function isDataUrl(url) {
return url.substr(0, 5) === "data:";
}

function parseDataUrl(url) {
const supportedEncodingRegexp = /^data:application\/json;([\w=:"-]+;)*base64,/;
const match = url.match(supportedEncodingRegexp);
if (match) {
const sourceMapStart = match[0].length;
const encodedSource = url.substr(sourceMapStart);
const source = atob(encodedSource);
return JSON.parse(source);
} else {
throw new Error("The encoding of the inline sourcemap is not supported");
}
}
11 changes: 11 additions & 0 deletions core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "css-to-js-sourcemap-core",
"version": "1.0.0",
"dependencies": {
"error-stack-parser": "^2.0.1",
"source-map": "^0.7.3",
"source-map-url": "^0.4.0",
"sourcemap-codec": "^1.4.1"
},
"license": "MIT"
}
16 changes: 16 additions & 0 deletions fixtures/app/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-env browser */

window.worker = new Worker("/worker.js");

const err1 = new Error("Line 5");
const err2 = new Error("Line 6");
const err3 = new Error("Line 7");

window.error1 = toErrorLikeObject(err1);
window.error2 = toErrorLikeObject(err2);
window.error3 = toErrorLikeObject(err3);

function toErrorLikeObject(err) {
const {stack, stacktrace, message} = err;
return {stack, stacktrace, message};
}
17 changes: 17 additions & 0 deletions fixtures/app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "css-to-js-sourcemap-fixture-app",
"version": "0.0.0-workspace",
"private": true,
"main": "server.js",
"scripts": {
"prepare": "webpack"
},
"dependencies": {
"sirv": "^0.1.2",
"css-to-js-sourcemap-worker": "*"
},
"devDependencies": {
"webpack": ">= 4",
"webpack-cli": "*"
}
}
59 changes: 59 additions & 0 deletions fixtures/app/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/* eslint-env node */

const http = require("http");
const path = require("path");
const sirv = require("sirv");

const workerPath = require.resolve("css-to-js-sourcemap-worker");

const assets = sirv(path.join(__dirname, "static"));
const worker = sirv(path.dirname(workerPath));

const routes = {
"/no-map": "/no-map.js",
"/inline-map": "/inline-map.js",
"/external-map": "/external-map.js",
};

function createServer() {
let blockNetwork = Promise.resolve();
let unblock = () => {};
const server = http.createServer((req, res) => {
blockNetwork.then(() => {
const script = routes[req.url];
if (script) {
res.setHeader("Content-Type", "text/html");
res.statusCode = 200;
return void res.end(template(script));
}
if (req.url === "/worker.js") {
return worker(req, res);
}
return assets(req, res);
});
});
server.blockAllRequests = () => {
blockNetwork = new Promise(resolve => {
unblock = resolve;
});
};
server.unblockAllRequests = () => {
unblock();
};
return server;
}

module.exports = createServer;

function template(url) {
return `<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<script src="${url}"></script>
</head>
<body>
</body>
</html>
`;
}
Loading

0 comments on commit db48893

Please sign in to comment.