From d0bc48b97ac962bef72d97f7e69bf297c4336096 Mon Sep 17 00:00:00 2001 From: Chunpeng Huo Date: Fri, 24 Jul 2020 10:40:51 +1000 Subject: [PATCH] wip feat(sass): support remote scss file close #39 --- client/src-worker/index.js | 4 +- client/src-worker/transpiler.js | 22 ++-- client/src-worker/transpilers/sass.js | 101 ++++++++++++++-- client/test-worker/transpilers/sass.spec.js | 122 ++++++++++++++++++-- 4 files changed, 217 insertions(+), 32 deletions(-) diff --git a/client/src-worker/index.js b/client/src-worker/index.js index 5942a54..8ea2524 100644 --- a/client/src-worker/index.js +++ b/client/src-worker/index.js @@ -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; 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..66cfe12 100644 --- a/client/src-worker/transpilers/sass.js +++ b/client/src-worker/transpilers/sass.js @@ -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']; @@ -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() { @@ -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; @@ -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: ''}; } diff --git a/client/test-worker/transpilers/sass.spec.js b/client/test-worker/transpilers/sass.spec.js index 778f0a4..e420da8 100644 --- a/client/test-worker/transpilers/sass.spec.js +++ b/client/test-worker/transpilers/sass.spec.js @@ -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(); @@ -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', @@ -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', @@ -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', @@ -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', @@ -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', @@ -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 = { @@ -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 @@ -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]); -}); \ 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', 'src/bar.sass']); + // Somehow sass.js sources content is scss format, not sass format. + // t.deepEqual(file.sourceMap.sourcesContent, [foo, bar]); +});