diff --git a/client/nodejs-stubs.js b/client/nodejs-stubs.js
index c499ccf..34548b6 100644
--- a/client/nodejs-stubs.js
+++ b/client/nodejs-stubs.js
@@ -1,3 +1,5 @@
+import '@aurelia/kernel';
+
global.HOST_NAMES = require('./host-names');
global.DUMBER_MODULE_LOADER_DIST = 'dumber-module-loader dist content';
global.fetch = require('node-fetch');
diff --git a/client/src-worker/transpiler.js b/client/src-worker/transpiler.js
index ca06da0..6828534 100644
--- a/client/src-worker/transpiler.js
+++ b/client/src-worker/transpiler.js
@@ -1,4 +1,5 @@
import path from 'path';
+import {inject} from 'aurelia-dependency-injection';
import {SvelteTranspiler} from './transpilers/svelte';
import {Au2Transpiler} from './transpilers/au2';
import {AuTsTranspiler} from './transpilers/au-ts';
@@ -7,17 +8,18 @@ import {SassTranspiler} from './transpilers/sass';
import {LessTranspiler} from './transpilers/less';
import {TextTranspiler} from './transpilers/text';
+@inject(
+ SvelteTranspiler,
+ Au2Transpiler,
+ AuTsTranspiler,
+ JsTranspiler,
+ SassTranspiler,
+ LessTranspiler,
+ TextTranspiler
+)
export class Transpiler {
- constructor() {
- this.transpilers = [
- new SvelteTranspiler(),
- new Au2Transpiler(),
- new AuTsTranspiler(),
- new JsTranspiler(),
- new SassTranspiler(),
- new LessTranspiler(),
- new TextTranspiler()
- ];
+ constructor(...transpilers) {
+ this.transpilers = transpilers;
}
findTranspiler(file, files) {
diff --git a/client/src-worker/transpilers/sass.js b/client/src-worker/transpilers/sass.js
index 2c4f90c..7e312ff 100644
--- a/client/src-worker/transpilers/sass.js
+++ b/client/src-worker/transpilers/sass.js
@@ -1,18 +1,79 @@
-import path from 'path';
+import {ext, parse, resolveModuleId} from 'dumber-module-loader/dist/id-utils';
import _ from 'lodash';
+import {inject} from 'aurelia-dependency-injection';
+import {CachePrimitives} from '../cache-primitives';
const EXTS = ['.scss', '.sass'];
function cleanSource(s) {
- const idx = s.indexOf('/sass/');
- if (idx === -1) return s;
- return s.slice(idx + 6);
+ if (s.startsWith('../sass/')) {
+ return s.slice(8);
+ }
+ if (s.startsWith('../')) {
+ return s.slice(3);
+ }
+ return s;
}
+export function possiblePaths(filePath) {
+ const parsed = parse(filePath);
+ const [packagePath, ...others] = parsed.parts;
+ if (others.length === 0) return [];
+
+ const base = others.pop();
+ const dir = _(others).map(o => o + '/').join('');
+
+ if (EXTS.indexOf(parsed.ext) !== -1 || parsed.ext === '.css') {
+ return {
+ packagePath,
+ filePaths: [
+ dir + base,
+ dir + base + '/_index.scss',
+ dir + base + '/_index.sass'
+ ]
+ };
+ }
+
+ return {
+ packagePath,
+ filePaths: [
+ dir + base + '.scss',
+ dir + base + '.sass',
+ dir + base + '.css',
+ dir + '_' + base + '.scss',
+ dir + '_' + base + '.sass',
+ dir + base + '/_index.scss',
+ dir + base + '/_index.sass'
+ ]
+ };
+}
+
+@inject(CachePrimitives)
export class SassTranspiler {
+ constructor(primitives) {
+ this.primitives = primitives;
+ }
+
match(file) {
- const ext = path.extname(file.filename);
- return EXTS.indexOf(ext) !== -1;
+ const e = ext(file.filename);
+ return EXTS.indexOf(e) !== -1;
+ }
+
+ async fetchRemoteFile(path) {
+ const possible = possiblePaths(path)
+
+ const packageJson = JSON.parse(
+ (await this.primitives.getJsdelivrFile(possible.packagePath, 'package.json')).contents
+ );
+
+ const packagePathWithVersion = possible.packagePath + '@' + packageJson.version;
+
+ for (const filePath of possible.filePaths) {
+ if (await this.primitives.doesJsdelivrFileExist(packagePathWithVersion, filePath)) {
+ return this.primitives.getJsdelivrFile(packagePathWithVersion, filePath);
+ }
+ }
+ throw new Error('No remote file found for ' + path);
}
_lazyLoad() {
@@ -21,7 +82,41 @@ export class SassTranspiler {
// https://github.com/sass/dart-sass/issues/25
// So I have to use sass.js (emscripted libsass) as it
// provided a fake fs layer.
- this._promise = import('sass.js/dist/sass.sync');
+ this._promise = import('sass.js/dist/sass.sync').then(Sass => {
+ // Add custom importer to handle import from npm packages.
+ Sass.importer((request, done) => {
+ if (
+ request.path ||
+ request.current.startsWith('.') ||
+ request.current.match(/^https?:\/\//)
+ ) {
+ // Sass.js already found a file,
+ // or it's definitely not a remote file,
+ // or it's a full url,
+ // let Sass.js to do its job.
+ done();
+ } else {
+ let remotePath = request.current;
+ if (request.previous.startsWith('/node_modules/')) {
+ remotePath = resolveModuleId(request.previous.slice(14), './' + request.current);
+ }
+
+ this.fetchRemoteFile(remotePath).then(
+ ({path, contents}) => {
+ done({
+ path: '/node_modules/' + path.slice(23),
+ content: contents
+ });
+ },
+ err => {
+ done({error: err.message});
+ }
+ );
+ }
+ });
+
+ return Sass;
+ });
}
return this._promise;
@@ -30,32 +125,37 @@ export class SassTranspiler {
async transpile(file, files) {
const {filename} = file;
if (!this.match(file)) throw new Error('Cannot use SassTranspiler for file: ' + filename);
- if (path.basename(filename).startsWith('_')) {
+
+ const parsed = parse(filename);
+ if (_.last(parsed.parts).startsWith('_')) {
// ignore sass partial
return;
}
const Sass = await this._lazyLoad();
- const ext = path.extname(filename);
-
const cssFiles = {};
_.each(files, f => {
- const ext = path.extname(f.filename);
- if (EXTS.indexOf(ext) !== -1 || ext === '.css') {
+ const e = ext(f.filename);
+ if (EXTS.indexOf(e) !== -1 || e === '.css') {
cssFiles[f.filename] = f.content;
}
});
- const newFilename = filename.slice(0, -ext.length) + '.css';
+ const newFilename = filename.slice(0, -parsed.ext.length) + '.css';
if (file.content.match(/^\s*$/)) {
return {filename: newFilename, content: ''};
}
return new Promise((resolve, reject) => {
Sass.writeFile(cssFiles, () => {
- Sass.compileFile(
- filename,
+ Sass.compile(
+ file.content,
+ {
+ indentedSyntax: parsed.ext === '.sass',
+ sourceMapRoot: '/',
+ inputPath: '/sass/' + filename
+ },
result => {
Sass.removeFile(Object.keys(cssFiles), () => {
if (result.status === 0) {
diff --git a/client/test-worker/transpiler.spec.js b/client/test-worker/transpiler.spec.js
index 0d2df68..31bfd8e 100644
--- a/client/test-worker/transpiler.spec.js
+++ b/client/test-worker/transpiler.spec.js
@@ -1,5 +1,18 @@
import test from 'tape';
+import {Container} from 'aurelia-dependency-injection';
import {Transpiler} from '../src-worker/transpiler';
+import {CachePrimitives} from '../src-worker/cache-primitives';
+
+const container = new Container();
+container.registerInstance(CachePrimitives, {
+ async getJsdelivrFile() {
+ throw new Error('should not get here');
+ },
+ async doesJsdelivrFileExist() {
+ return false;
+ }
+});
+const jt = container.get(Transpiler);
const p = {
filename: 'package.json',
@@ -7,7 +20,6 @@ const p = {
};
test('Transpiler transpiles au1 ts file', async t => {
- const jt = new Transpiler();
const code = `import {autoinject, bindable} from 'aurelia-framework';
@autoinject
export class Foo {
@@ -31,7 +43,6 @@ export class Foo {
});
test('Transpiler transpiles jsx file in inferno way with fragment', async t => {
- const jt = new Transpiler();
const code = `const descriptions = items.map(item => (
<>
{item.name}
@@ -54,7 +65,6 @@ test('Transpiler transpiles jsx file in inferno way with fragment', async t => {
});
test('Transpiler transpile scss file', async t => {
- const jt = new Transpiler();
const code = '.a { .b { color: red; } }';
const f = {
filename: 'src/foo.scss',
@@ -71,7 +81,6 @@ test('Transpiler transpile scss file', async t => {
});
test('Transpiler transpiles supported text file', async t => {
- const jt = new Transpiler();
const code = 'lorem';
const file = await jt.transpile({
filename: 'src/foo/bar.html',
@@ -85,7 +94,6 @@ test('Transpiler transpiles supported text file', async t => {
});
test('Transpiler cannot transpile binary file', async t => {
- const jt = new Transpiler();
t.equal(await jt.transpile({
filename: 'src/foo.jpg',
content: ''
@@ -93,7 +101,6 @@ test('Transpiler cannot transpile binary file', async t => {
});
test('Transpiler transpiles au2 file', async t => {
- const jt = new Transpiler();
const code = `export class Foo {
public name: string;
}
@@ -118,7 +125,6 @@ test('Transpiler transpiles au2 file', async t => {
});
test('eTranspiler transpiles svelte file with scss', async t => {
- const jt = new Transpiler();
const code = `
diff --git a/client/test-worker/transpilers/sass.spec.js b/client/test-worker/transpilers/sass.spec.js
index 778f0a4..92aa565 100644
--- a/client/test-worker/transpilers/sass.spec.js
+++ b/client/test-worker/transpilers/sass.spec.js
@@ -1,8 +1,76 @@
import test from 'tape';
-import {SassTranspiler} from '../../src-worker/transpilers/sass';
+import {SassTranspiler, possiblePaths} from '../../src-worker/transpilers/sass';
+import {JSDELIVR_PREFIX} from '../../src-worker/cache-primitives';
+
+const primitives = {
+ async getJsdelivrFile(packageWithVersion, filePath) {
+ if (packageWithVersion === '@scope/foo@1.0.0') {
+ if (filePath === 'dist/_button.scss') {
+ return {
+ path: `${JSDELIVR_PREFIX}@scope/foo/dist/_button.scss`,
+ contents: '@import "mixin";'
+ };
+ } else if (filePath === 'dist/_mixin.scss') {
+ return {
+ path: `${JSDELIVR_PREFIX}@scope/foo/dist/_mixin.scss`,
+ contents: '$green: #34C371;'
+ };
+ }
+ } else if (packageWithVersion === '@scope/foo') {
+ if (filePath === 'package.json') {
+ return {
+ path: `${JSDELIVR_PREFIX}@scope/foo/package.json`,
+ contents: '{"name":"@scope/foo","version":"1.0.0"}'
+ };
+ }
+ }
+ },
+ async doesJsdelivrFileExist(packageWithVersion, filePath) {
+ if (packageWithVersion === '@scope/foo@1.0.0') {
+ return filePath === 'dist/_button.scss' ||
+ filePath === 'dist/_mixin.scss';
+ }
+ },
+};
+
+test('possiblePaths returns nothing for non-remote path', t => {
+ t.deepEqual(
+ possiblePaths('foo'),
+ []
+ );
+ t.end();
+});
+
+test('possiblePaths returns index paths', t => {
+ t.deepEqual(
+ possiblePaths('foo/bar.scss'),
+ [
+ {packagePath: 'foo', filePath: 'bar.scss'},
+ {packagePath: 'foo', filePath: 'bar.scss/_index.scss'},
+ {packagePath: 'foo', filePath: 'bar.scss/_index.sass'}
+ ]
+ );
+ t.end();
+});
+
+test('possiblePaths returns index paths and partial paths', t => {
+ t.deepEqual(
+ possiblePaths('@scope/foo/dist/bar'),
+ [
+ {packagePath: '@scope/foo', filePath: 'dist/bar.scss'},
+ {packagePath: '@scope/foo', filePath: 'dist/bar.sass'},
+ {packagePath: '@scope/foo', filePath: 'dist/bar.css'},
+ {packagePath: '@scope/foo', filePath: 'dist/_bar.scss'},
+ {packagePath: '@scope/foo', filePath: 'dist/_bar.sass'},
+ {packagePath: '@scope/foo', filePath: 'dist/bar/_index.scss'},
+ {packagePath: '@scope/foo', filePath: 'dist/bar/_index.sass'}
+ ]
+ );
+ t.end();
+});
test('SassTranspiler matches sass/scss files', t => {
- const jt = new SassTranspiler();
+ const jt = new SassTranspiler(primitives);
t.ok(jt.match({filename: 'src/foo.sass', content: ''}));
t.ok(jt.match({filename: 'src/foo.scss', content: ''}));
t.end();
@@ -17,7 +85,7 @@ test('SassTranspiler does not match other files', t => {
});
test('SassTranspiler transpile scss file', async t => {
- const jt = new SassTranspiler();
+ const jt = new SassTranspiler(primitives);
const code = '.a { .b { color: red; } }';
const f = {
filename: 'src/foo.scss',
@@ -33,7 +101,7 @@ test('SassTranspiler transpile scss file', async t => {
});
test('SassTranspiler transpile empty scss file', async t => {
- const jt = new SassTranspiler();
+ const jt = new SassTranspiler(primitives);
const code = '\n\t\n';
const f = {
filename: 'src/foo.scss',
@@ -47,7 +115,7 @@ test('SassTranspiler transpile empty scss file', async t => {
});
test('SassTranspiler reject broken scss file', async t => {
- const jt = new SassTranspiler();
+ const jt = new SassTranspiler(primitives);
const code = '.a {';
const f = {
filename: 'src/foo.scss',
@@ -62,7 +130,7 @@ test('SassTranspiler reject broken scss file', async t => {
});
test('SassTranspiler cannot tranpile other file', async t => {
- const jt = new SassTranspiler();
+ const jt = new SassTranspiler(primitives);
try {
await jt.transpile({
filename: 'src/foo.js',
@@ -75,7 +143,7 @@ test('SassTranspiler cannot tranpile other file', async t => {
});
test('SassTranspiler ignore scss partial', async t => {
- const jt = new SassTranspiler();
+ const jt = new SassTranspiler(primitives);
const code = '.a { .b { color: red; } }';
const f = {
filename: 'src/_foo.scss',
@@ -86,7 +154,7 @@ test('SassTranspiler ignore scss partial', async t => {
});
test('SassTranspiler transpile scss file with partial import', async t => {
- const jt = new SassTranspiler();
+ const jt = new SassTranspiler(primitives);
const foo = '@import "variables";\n.a { .b { color: $red; } }';
const variables = '$red: #f00;';
const f = {
@@ -108,7 +176,7 @@ test('SassTranspiler transpile scss file with partial import', async t => {
});
test('SassTranspiler transpile sass file with import', async t => {
- const jt = new SassTranspiler();
+ const jt = new SassTranspiler(primitives);
const foo = `@import "bar"
.a
.b
@@ -134,4 +202,49 @@ test('SassTranspiler transpile sass file with import', async t => {
t.deepEqual(file.sourceMap.sources, ['src/foo.sass', 'src/bar.sass']);
// Somehow sass.js sources content is scss format, not sass format.
// t.deepEqual(file.sourceMap.sourcesContent, [foo, bar]);
-});
\ No newline at end of file
+});
+
+test('SassTranspiler rejects missing import', async t => {
+ const jt = new SassTranspiler(primitives);
+ const foo = `@import "bar"
+.a
+ .b
+ color: red
+`;
+ const f = {
+ filename: 'src/foo.sass',
+ content: foo
+ };
+ try {
+ await jt.transpile(f, [f]);
+ t.fail('should not pass');
+ } catch (e) {
+ t.pass(e.message);
+ }
+});
+
+test('SassTranspiler transpile sass file with remote import', async t => {
+ const jt = new SassTranspiler(primitives);
+ const foo = `@import "@scope/foo/dist/button"
+.a
+ .b
+ color: $green;
+`;
+ const f = {
+ filename: 'src/foo.sass',
+ content: foo
+ };
+ const file = await jt.transpile(f, [f]);
+
+ t.equal(file.filename, 'src/foo.css');
+ t.ok(file.content.includes('.a .b'));
+ t.ok(file.content.includes('#34C371'));
+ t.equal(file.sourceMap.file, 'src/foo.css');
+ t.deepEqual(file.sourceMap.sources, [
+ 'src/foo.sass',
+ 'node_modules/@scope/foo/dist/_button.scss',
+ 'node_modules/@scope/foo/dist/_mixin.scss',
+ ]);
+ // Somehow sass.js sources content is scss format, not sass format.
+ // t.deepEqual(file.sourceMap.sourcesContent, [foo, bar]);
+});