Skip to content

Commit

Permalink
wip feat(sass): support remote scss file
Browse files Browse the repository at this point in the history
close #39
  • Loading branch information
3cp committed Jul 24, 2020
1 parent ef85842 commit d0bc48b
Show file tree
Hide file tree
Showing 4 changed files with 217 additions and 32 deletions.
4 changes: 2 additions & 2 deletions client/src-worker/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import {Container} from 'aurelia-dependency-injection';
methods.forEach(m => patch(m));
})();

const container = new Container();
const session = container.get(DumberSession);
diContainer = new Container();
const session = diContainer.get(DumberSession);

onmessage = async function(event) {
var action = event.data;
Expand Down
22 changes: 12 additions & 10 deletions client/src-worker/transpiler.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down
101 changes: 91 additions & 10 deletions client/src-worker/transpilers/sass.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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'];

Expand All @@ -9,10 +11,51 @@ function cleanSource(s) {
return s.slice(idx + 6);
}

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, filePath: dir + base},
{packagePath, filePath: dir + base + '/_index.scss'},
{packagePath, filePath: dir + base + '/_index.sass'}
];
}

return [
{packagePath, filePath: dir + base + '.scss'},
{packagePath, filePath: dir + base + '.sass'},
{packagePath, filePath: dir + base + '.css'},
{packagePath, filePath: dir + '_' + base + '.scss'},
{packagePath, filePath: dir + '_' + base + '.sass'},
{packagePath, filePath: dir + base + '/_index.scss'},
{packagePath, filePath: 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) {
for (const {packagePath, filePath} of possiblePaths(path)) {
if (await this.primitives.doesJsdelivrFileExist(packagePath, filePath)) {
return this.primitives.getJsdelivrFile(packagePath, filePath);
}
}
throw new Error('No remote file found for ' + path);
}

_lazyLoad() {
Expand All @@ -21,7 +64,45 @@ 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 {
console.log('request.current', request.current);
console.log('request.previous', request.previous);

let remotePath = request.current;
if (!request.previous.startsWith('/')) {
// local file system starts with /sass/
remotePath = resolveModuleId(request.previous, './' + request.current);
}
console.log('remotePath', remotePath);

this.fetchRemoteFile(remotePath).then(
({path, contents}) => {
console.log("got remote " + path + ' ' + contents);
done({path: remotePath, content: contents});
},
err => {
console.log(err);
done({error: err.message});
}
);
}
});

return Sass;
});
}

return this._promise;
Expand All @@ -30,24 +111,24 @@ 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: ''};
}
Expand Down
122 changes: 112 additions & 10 deletions client/test-worker/transpilers/sass.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,69 @@
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') {
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;'
};
}
}
},
async doesJsdelivrFileExist(packageWithVersion, filePath) {
if (packageWithVersion === '@scope/foo') {
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();
Expand All @@ -17,7 +78,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',
Expand All @@ -33,7 +94,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',
Expand All @@ -47,7 +108,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',
Expand All @@ -62,7 +123,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',
Expand All @@ -75,7 +136,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',
Expand All @@ -86,7 +147,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 = {
Expand All @@ -108,7 +169,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
Expand All @@ -134,4 +195,45 @@ 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]);
});
});

// 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', 'src/bar.sass']);
// Somehow sass.js sources content is scss format, not sass format.
// t.deepEqual(file.sourceMap.sourcesContent, [foo, bar]);
});

0 comments on commit d0bc48b

Please sign in to comment.