Skip to content

Commit

Permalink
chore(jsdoc): document @stencil/sass (#225)
Browse files Browse the repository at this point in the history
this commit adds jsdoc to the @stencil/sass package to help with
transferring knowledge about the package/making it easier to pick up to
work on. this is being done prior to the upgrade to sass 1.58.0, which
will lead to type changes within the project. this commit is the result
of the exercise of the author refamiliarizing themself with the project
a bit, rather than a direct dependency of the aforementioned update.
  • Loading branch information
rwaskiewicz authored Feb 15, 2023
1 parent c39c967 commit 9e5519a
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 20 deletions.
39 changes: 34 additions & 5 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
import { SassException } from 'sass';
import * as d from './declarations';

export function loadDiagnostic(context: d.PluginCtx, sassError: SassException, filePath: string) {
/**
* Generates a diagnostic as a result of an error originating from Sass.
*
* This function mutates the provided context by pushing the generated diagnostic to the context's collection of
* diagnostics.
*
* @param context the compilation context that the plugin has access to
* @param sassError the Sass error to create a diagnostic from
* @param filePath the path of the file that led to an error being raised
* @returns the created diagnostic, or `null` if one could not be generated
*/
export function loadDiagnostic(context: d.PluginCtx, sassError: SassException, filePath: string): d.Diagnostic | null {
if (sassError == null || context == null) {
return null;
}
Expand Down Expand Up @@ -35,7 +46,7 @@ export function loadDiagnostic(context: d.PluginCtx, sassError: SassException, f

if (errorLineIndex > -1) {
try {
const sourceText = context.fs.readFileSync(diagnostic.absFilePath) as string;
const sourceText = context.fs.readFileSync(diagnostic.absFilePath);
const srcLines = sourceText.split(/\r?\n/);

const errorLine: d.PrintLine = {
Expand Down Expand Up @@ -101,23 +112,41 @@ export function loadDiagnostic(context: d.PluginCtx, sassError: SassException, f
return diagnostic;
}

function formatCode(input: number) {
/**
* Helper function for converting a number error code to a string
* @param input the numeric error code to convert
* @returns the stringified error code
*/
function formatCode(input: number): string {
let output = '';
if (input != null) {
output = String(input);
}
return output;
}

function formatMessage(input: string) {
/**
* Splits up a message from Sass, returning all input prior to the first '╷' character.
* If no such character exists, the entire original message will be returned.
* @param input the Sass message to split
* @returns the split message
*/
function formatMessage(input: string): string {
let output = '';
if (typeof input === 'string') {
output = input.split('╷')[0];
}
return output;
}

function formatFileName(rootDir: string, fileName: string) {
/**
* Formats the provided filename, by stripping the provided root directory out of the filename, and limiting the
* display string to 80 characters
* @param rootDir the root directory to strip out of the provided filename
* @param fileName the filename to format for pretty printing
* @returns the formatted filename
*/
function formatFileName(rootDir: string, fileName: string): string {
if (!rootDir || !fileName) return '';

fileName = fileName.replace(rootDir, '');
Expand Down
21 changes: 20 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,29 @@ import * as d from './declarations';
import { loadDiagnostic } from './diagnostics';
import { createResultsId, getRenderOptions, usePlugin } from './util';

/**
* The entrypoint of the Stencil Sass plugin
*
* This function creates & configures the plugin to be used by consuming Stencil projects
*
* For configuration details, please see the [GitHub README](https://github.com/ionic-team/stencil-sass).
*
* @param opts options to configure the plugin
* @return the configured plugin
*/
export function sass(opts: d.PluginOptions = {}): d.Plugin {
return {
name: 'sass',
pluginType: 'css',
transform(sourceText, fileName, context) {
/**
* Performs the Sass file compilation
* @param sourceText the contents of the Sass file to compile
* @param fileName the name of the Sass file to compile
* @param context a runtime context supplied by Stencil, providing access to the current configuration, an
* in-memory FS, etc.
* @returns the results of the Sass file compilation
*/
transform(sourceText: string, fileName: string, context: d.PluginCtx): Promise<d.PluginTransformResults> {
if (!usePlugin(fileName)) {
return null;
}
Expand All @@ -27,6 +45,7 @@ export function sass(opts: d.PluginOptions = {}): d.Plugin {

return new Promise<d.PluginTransformResults>((resolve) => {
try {
// invoke sass' compiler at this point
render(renderOpts, (err, sassResult) => {
if (err) {
loadDiagnostic(context, err, fileName);
Expand Down
72 changes: 58 additions & 14 deletions src/util.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,50 @@
import * as d from './declarations';
import * as path from 'path';
import { Importer } from 'sass';

import { Importer, ImporterReturnType } from 'sass';

/**
* Determine if the Sass plugin should be applied, based on the provided `fileName`
*
* @param fileName the name of a file to potentially transform
* @returns `true` if the name of the file ends with a sass extension (.scss, .sass), case insensitive. `false`
* otherwise
*/
export function usePlugin(fileName: string) {
if (typeof fileName === 'string') {
return /(\.scss|\.sass)$/i.test(fileName);
}
return true;
}

export function getRenderOptions(opts: d.PluginOptions, sourceText: string, fileName: string, context: d.PluginCtx) {
// create a copy of the original sass config so we don't change it
/**
* Build a list of options to provide to Sass' `render` API.
* @param opts the options provided to the plugin within a Stencil configuration file
* @param sourceText the source text of the file to transform
* @param fileName the name of the file to transform
* @param context the runtime context being used by the plugin
* @returns the generated/normalized plugin options
*/
export function getRenderOptions(
opts: d.PluginOptions,
sourceText: string,
fileName: string,
context: d.PluginCtx
): d.PluginOptions {
// create a copy of the original sass config, so we don't modify the one provided
const renderOpts = Object.assign({}, opts);

// always set "data" from the source text
renderOpts.data = sourceText;

// activate indented syntax if the file extension is .sass
// activate indented syntax if the file extension is .sass.
// this needs to be set prior to injecting global sass (as the syntax affects the import terminator)
renderOpts.indentedSyntax = /(\.sass)$/i.test(fileName);

// create a copy of the original path config, so we don't modify the one provided
renderOpts.includePaths = Array.isArray(opts.includePaths) ? opts.includePaths.slice() : [];

// add the directory of the source file to includePaths
renderOpts.includePaths.push(path.dirname(fileName));

// ensure each of the includePaths is an absolute path
renderOpts.includePaths = renderOpts.includePaths.map((includePath) => {
if (path.isAbsolute(includePath)) {
return includePath;
Expand All @@ -32,15 +53,17 @@ export function getRenderOptions(opts: d.PluginOptions, sourceText: string, file
return path.resolve(context.config.rootDir, includePath);
});

const injectGlobalPaths = Array.isArray(opts.injectGlobalPaths) ? opts.injectGlobalPaths.slice() : [];
// create a copy of the original global config of paths to inject, so we don't modify the one provided.
// this is a Stencil-specific configuration, and not a part of the Sass API.
const injectGlobalPaths: string[] = Array.isArray(opts.injectGlobalPaths) ? opts.injectGlobalPaths.slice() : [];

if (injectGlobalPaths.length > 0) {
// automatically inject each of these paths into the source text
// Automatically inject each of these paths into the source text.
// This is accomplished by prepending the global stylesheets to the file being processed.
const injectText = injectGlobalPaths
.map((injectGlobalPath) => {
if (!path.isAbsolute(injectGlobalPath)) {
// convert any relative paths to absolute paths relative to the project root

if (context.sys && typeof context.sys.normalizePath === 'function') {
// context.sys.normalizePath added in stencil 1.11.0
injectGlobalPath = context.sys.normalizePath(path.join(context.config.rootDir, injectGlobalPath));
Expand Down Expand Up @@ -73,7 +96,14 @@ export function getRenderOptions(opts: d.PluginOptions, sourceText: string, file
importers.push(...renderOpts.importer);
}

const importer: Importer = (url, _prev, done) => {
/**
* Create a handler for loading files when a `@use` or `@import` rule is encountered for loading a path prefixed
* with a tilde (~). Such imports indicate that the module should be resolved from the `node_modules` directory.
* @param url the path to the module to load
* @param _prev Unused - typically, this is a string identifying the stylesheet that contained the @use or @import.
* @param done a callback to return the path to the resolved path
*/
const importer: Importer = (url: string, _prev: string, done: (data: ImporterReturnType) => void): void => {
if (typeof url === 'string') {
if (url.startsWith('~')) {
try {
Expand All @@ -85,7 +115,7 @@ export function getRenderOptions(opts: d.PluginOptions, sourceText: string, file
moduleId: m.moduleId,
containingFile: m.filePath,
})
.then((resolved) => {
.then((resolved: d.ResolveModuleIdResults) => {
if (resolved.pkgDirPath) {
const resolvedPath = path.join(resolved.pkgDirPath, m.filePath);
done({
Expand Down Expand Up @@ -116,7 +146,15 @@ export function getRenderOptions(opts: d.PluginOptions, sourceText: string, file
return renderOpts;
}

export function createResultsId(fileName: string) {
/**
* Replaces the extension with the provided file name with 'css'.
*
* If the file does not have an extension, no transformation will be applied.
*
* @param fileName the name of the file whose extension should be replaced
* @returns the updated filename, using 'css' as the file extension
*/
export function createResultsId(fileName: string): string {
// create what the new path is post transform (.css)
const pathParts = fileName.split('.');
pathParts[pathParts.length - 1] = 'css';
Expand Down Expand Up @@ -154,7 +192,12 @@ export function normalizePath(str: string) {
return str;
}

export function getModuleId(orgImport: string) {
/**
* Split an import path into a module ID and file path
* @param orgImport the import path to split
* @returns a module id and the filepath under that module id
*/
export function getModuleId(orgImport: string): { moduleId: string; filePath: string } {
if (orgImport.startsWith('~')) {
orgImport = orgImport.substring(1);
}
Expand All @@ -165,6 +208,7 @@ export function getModuleId(orgImport: string) {
};

if (orgImport.startsWith('@') && splt.length > 1) {
// we have a scoped package, it's module includes the word following the first slash
m.moduleId = splt.slice(0, 2).join('/');
m.filePath = splt.slice(2).join('/');
} else {
Expand Down

0 comments on commit 9e5519a

Please sign in to comment.