Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Migrate to new sass API #9966

Merged
merged 3 commits into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ const path = require('path')
module.exports = {
includePaths: [
path.join(__dirname, "include-path")
]
],
silenceDeprecations: ['legacy-js-api']
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const path = require('path')

module.exports = {
loadPaths: [
path.join(__dirname, "include-path")
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.included
color: red
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@import "style.sass"
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"private": true
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module.exports = {
data: "$color: red;"
data: "$color: red;",
silenceDeprecations: ['legacy-js-api']
}
18 changes: 17 additions & 1 deletion packages/core/integration-tests/test/sass.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ describe('sass', function () {
assert(css.includes('.external'));
});

it('should support imports from includePaths', async function () {
it('should support imports from includePaths (legacy)', async function () {
let b = await bundle(
path.join(__dirname, '/integration/sass-include-paths-import/index.sass'),
);
Expand All @@ -300,6 +300,22 @@ describe('sass', function () {
assert(css.includes('.included'));
});

it('should support imports from loadPaths (modern)', async function () {
let b = await bundle(
path.join(__dirname, '/integration/sass-load-paths-import/index.sass'),
);

assertBundles(b, [
{
name: 'index.css',
assets: ['index.sass'],
},
]);

let css = await outputFS.readFile(path.join(distDir, 'index.css'), 'utf8');
assert(css.includes('.included'));
});

it('should support package.json exports', async function () {
let b = await bundle(
path.join(__dirname, '/integration/sass-exports/index.sass'),
Expand Down
2 changes: 1 addition & 1 deletion packages/transformers/sass/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@
"dependencies": {
"@parcel/plugin": "2.12.0",
"@parcel/source-map": "^2.1.1",
"sass": "^1.38.0"
"sass": "^1.79.4"
}
}
234 changes: 80 additions & 154 deletions packages/transformers/sass/src/SassTransformer.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
// @flow
import {Transformer} from '@parcel/plugin';
import path from 'path';
import {EOL} from 'os';
import SourceMap from '@parcel/source-map';
import sass from 'sass';
import {promisify} from 'util';

// E.g: ~library/file.sass
const NODE_MODULE_ALIAS_RE = /^~[^/\\]/;
import {transformLegacy} from './legacy';
import {transformModern} from './modern';

export default (new Transformer({
async loadConfig({config, options}) {
Expand All @@ -27,171 +22,102 @@ export default (new Transformer({
configResult = {};
}

// Resolve relative paths from config file
if (configFile && configResult.includePaths) {
configResult.includePaths = configResult.includePaths.map(p =>
path.resolve(path.dirname(configFile.filePath), p),
);
}

if (configResult.importer === undefined) {
configResult.importer = [];
} else if (!Array.isArray(configResult.importer)) {
configResult.importer = [configResult.importer];
}

// Always emit sourcemap
configResult.sourceMap = true;
// sources are created relative to the directory of outFile
configResult.outFile = path.join(options.projectRoot, 'style.css.map');
configResult.omitSourceMapUrl = true;
configResult.sourceMapContents = false;
let version = detectVersion(configResult);

return configResult;
},
if (version === 'legacy') {
// Resolve relative paths from config file
if (configFile && configResult.includePaths) {
configResult.includePaths = configResult.includePaths.map(p =>
path.resolve(path.dirname(configFile.filePath), p),
);
}

async transform({asset, options, config, resolve}) {
let rawConfig = config ?? {};
let sassRender = promisify(sass.render.bind(sass));
let css;
try {
let code = await asset.getCode();
let result = await sassRender({
...rawConfig,
file: asset.filePath,
data: rawConfig.data ? rawConfig.data + EOL + code : code,
importer: [
...rawConfig.importer,
resolvePathImporter({
asset,
resolve,
includePaths: rawConfig.includePaths,
options,
}),
],
indentedSyntax:
typeof rawConfig.indentedSyntax === 'boolean'
? rawConfig.indentedSyntax
: asset.type === 'sass',
});

css = result.css;
for (let included of result.stats.includedFiles) {
if (included !== asset.filePath) {
asset.invalidateOnFileChange(included);
}
if (configResult.importer === undefined) {
configResult.importer = [];
} else if (!Array.isArray(configResult.importer)) {
configResult.importer = [configResult.importer];
}

if (result.map != null) {
let map = new SourceMap(options.projectRoot);
map.addVLQMap(JSON.parse(result.map));
asset.setMap(map);
// Always emit sourcemap
configResult.sourceMap = true;
// sources are created relative to the directory of outFile
configResult.outFile = path.join(options.projectRoot, 'style.css.map');
configResult.omitSourceMapUrl = true;
configResult.sourceMapContents = false;
} else if (version === 'modern') {
// Resolve relative paths from config file
if (configFile && configResult.loadPaths) {
configResult.loadPaths = configResult.loadPaths.map(p =>
path.resolve(path.dirname(configFile.filePath), p),
);
}
} catch (err) {
// Adapt the Error object for the reporter.
err.fileName = err.file;
err.loc = {
line: err.line,
column: err.column,
};

throw err;

// Always emit sourcemap
configResult.sourceMap = true;
}

asset.type = 'css';
asset.setCode(css);
return [asset];
return {version, config: configResult};
},
}): Transformer);

function resolvePathImporter({asset, resolve, includePaths, options}) {
// This is a reimplementation of the Sass resolution algorithm that uses Parcel's
// FS and tracks all tried files so they are watched for creation.
async function resolvePath(
url,
prev,
): Promise<{filePath: string, contents: string, ...} | void> {
/*
Imports are resolved by trying, in order:
* Loading a file relative to the file in which the `@import` appeared.
* Each custom importer.
* Loading a file relative to the current working directory (This rule doesn't really make sense for Parcel).
* Each load path in `includePaths`
* Each load path specified in the `SASS_PATH` environment variable, which should be semicolon-separated on Windows and colon-separated elsewhere.

See: https://sass-lang.com/documentation/js-api#importer
See also: https://github.com/sass/dart-sass/blob/006e6aa62f2417b5267ad5cdb5ba050226fab511/lib/src/importer/node/implementation.dart
*/

let paths = [path.dirname(prev)];
if (includePaths) {
paths.push(...includePaths);
async transform({asset, options, config: {version, config}, resolve}) {
if (version === 'legacy') {
await transformLegacy(asset, config, resolve, options);
} else {
await transformModern(asset, config, resolve, options);
}

asset.invalidateOnEnvChange('SASS_PATH');
if (options.env.SASS_PATH) {
paths.push(
...options.env.SASS_PATH.split(
process.platform === 'win32' ? ';' : ':',
).map(p => path.resolve(options.projectRoot, p)),
);
}
return [asset];
},
}): Transformer);

const urls = [url];
const urlFileName = path.basename(url);
if (urlFileName[0] !== '_') {
urls.push(path.join(path.dirname(url), `_${urlFileName}`));
function detectVersion(config: any) {
for (let legacyOption of [
'data',
'indentType',
'indentWidth',
'linefeed',
'outputStyle',
'importer',
'pkgImporter',
'includePaths',
'omitSourceMapUrl',
'outFile',
'sourceMapContents',
'sourceMapEmbed',
'sourceMapRoot',
]) {
if (config[legacyOption] != null) {
return 'legacy';
}
}

if (url[0] !== '~') {
for (let p of paths) {
for (let u of urls) {
const filePath = path.resolve(p, u);
try {
const contents = await asset.fs.readFile(filePath, 'utf8');
return {
filePath,
contents,
};
} catch (err) {
asset.invalidateOnFileCreate({filePath});
}
}
}
for (let modernOption of [
'loadPaths',
'sourceMapIncludeSources',
'style',
'importers',
]) {
if (config[modernOption] != null) {
return 'modern';
}
}

// If none of the default sass rules apply, try Parcel's resolver.
for (let u of urls) {
if (NODE_MODULE_ALIAS_RE.test(u)) {
u = u.slice(1);
}
try {
const filePath = await resolve(prev, u, {
packageConditions: ['sass', 'style'],
});
if (filePath) {
const contents = await asset.fs.readFile(filePath, 'utf8');
return {filePath, contents};
}
} catch (err) {
continue;
if (typeof config.sourceMap === 'string') {
return 'legacy';
}

if (
config.functions &&
typeof config.functions === 'object' &&
Object.keys(config.functions).length > 0
) {
for (let key in config.functions) {
let fn = config.functions[key];
if (typeof fn === 'function' && fn.length > 1) {
return 'legacy';
}
}
}

return function (rawUrl, prev, done) {
const url = rawUrl.replace(/^file:\/\//, '');
resolvePath(url, prev)
.then(resolved => {
if (resolved) {
done({
file: resolved.filePath,
contents: resolved.contents,
});
} else {
done();
}
})
.catch(done);
};
return 'modern';
}
Loading
Loading