npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

grunt-single-file-component

v0.0.21

Published

Grunt Single File Component

Readme

grunt-single-file-component

A small grunt task which lets you keep all the assets of your component in a (custom) single file

NPM

Build Status

Inspired by VUE.

Getting Started

If you haven't used Grunt before, be sure to check out the Getting Started guide, as it explains how to create a Gruntfile as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command:

npm install grunt-single-file-component --save-dev

One the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript:

grunt.loadNpmTasks('grunt-single-file-component');

Options

dstBase (optional)

Type: string

Default: undefined

The base folder of all the (relative) dst paths.

exts (optional)

Type: object

Default:

{
    template: '.html',
    script: '.js',
    style: '.css'		
}

Defines the file extension for the output files if dst is a directory. Notes:

  • The object you passed will be merged with the defaults.
  • Dot prefix can be omitted.

DEPRECATED use processor.ext instead!

processors

Type: object

Requires or defines processors.

About processors in general

Basically processors are functions to do the transformation of the node content. During transpiling the content of each component node will be passed to the corresponding processor (identified by the lang or processor [deprecated] attribute).

Processors have only one parameter:

  • content [string]: The content of the current node to be transpiled
  • retVal [string | Promise<string>]: The transpiled content to be written out

They may have the following properties:

  • id: The unique id of the processor (e.g. "pug")
  • ext: The extension of the output file (e.g. ".html"). Notes:
    1. If presents it overrides the corresponding exts option.
    2. Dot prefix can be omitted.
  • onTranspileStart: An optional hook to be executed before the transpiling process (see below)
  • onTranspileEnd: An optional hook to be executed after the transpiling process
Defining processors

You can define a processor by adding an entry to the processors object in the form of id: processor. In this case you can omit the id property.

Example:

processors: {
  pug: require('pug').render 
}

or

const renderPug = require('pug').render;
.
.
.
processors: {
  pug: Object.assign(src => renderPug(src), {ext: '.html'}) 
}
Requiring processors

You can require a processor by adding an entry in the form of idOrPathOfFactory: optionsObject. In this case the system tries to load and execute the processor factory with the given options. Processor factory must return a processor function.

Example:

pug-processor.js:

const pug = require('pug');

module.exports = function pugProcessorFactory({basedir, scope}) {
    return Object.assign(function pugProcessor(src) {
        return pug.compile(src, {basedir, pretty: true})(scope);
    }, {id: 'pug', ext: '.html'});
};

gruntfile.js:

processors: {
  'pug-processor': {basedir: '...', scope: {}}
}

onTranspileStart (optional)

Type: function | function[]

Default: []

Fired before transpiling with the following parameters:

  • fileName: The current component file
  • nodesToProcess: The nodes about to processing

onTranspileEnd (optional)

Type: function | function[]

Default: []

Fired after transpiling with the following parameters:

  • fileName: The current component file
  • nodesProcessed: The successfully processed nodes

quiet (optional)

Type: boolean

Default: false

Tells the system whether or not output messages to the console.

Processor context

Each processor has its own context (accessible via this) during execution. This context has the following properties:

name

Type: string

The name of the current node (e.g. "template").

dst

Type: string

The destination file (of the output). Valid only if the dst attribute is set on the current node.

attrs

Type: object

All the attributes (e.g. "dst") of the current node (including custom ones).

content

Type: string

The content of the current node.

processor

Type: function

The processor currently being executed.

startIndex

Type: int

The start index of the current node.

endIndex

Type: int

The end index of the current node.

nodeStart

Type: int

The start line of the current node.

nodeEnd

Type: int

The end line of the current node.

contentStart

Type: int

The start line of the content.

contentEnd

Type: int

The end line of the content.

Usage Example

In your project's Gruntfile, add a section named sfc to the data object passed into grunt.initConfig():

grunt.initConfig({
    sfc: {
        your_target: {
            src: ['dummy.component'],
            options: {
                dstBase: '<%= project.dirs.dist %>',
                processors: {
                    pug: require('pug').render,
                    js: content => {
                        // do something with the content
                        return content;
                    },
                    .
                    .
                    .
                }
            }
        }
    }
});

dummy.component:

<!-- 
  Notes:
    0) Node names must be unique (per component)
    1) If "lang" is omitted the raw node content will be used
    2) If "dst" is omitted the output of the processor will be treated as void
    3) The output file will be named "dummy.html" (because "dst" is a folder)
    4) Without "dstBase" "dst" should be "<%= project.dirs.dist %>/views/"
-->
<template lang="pug" dst="views/">
.foo
  .bar XxX
</template>

<!--
  Notes:
    0) The output file will be named "logic.js" (because "dst" is a file)
-->
<script lang="js" dst="scripts/logic.js">
console.log('kerekesfacapa');
</script>
.
.
.

Getting the correct line numbers (example)

In the following snippet we're using ESLint to validate .js files. Since the linter knows nothing about our component we have to fix the report line numbers manually:

const eslint = new ESLintCLI(require('eslint'));
.
.
.
grunt.initConfig({
    sfc: {
        your_target: {
            src: ['*.component'],
            options: {
                processors: {
                    js: function(content){
                        eslint.validate(content, this.nodeStart);
                        return content;
                    },
                    .
                    .
                    .
                }
            }
        }
    }
});
.
.
.
function ESLintCLI({CLIEngine}){
    const
        engine = new CLIEngine({
            outputFile:  false,
            quiet:       false,
            maxWarnings: -1,
            failOnError: true,
            configFile:  'eslint.json'
        }),
        formatter = CLIEngine.getFormatter('xXx');

    this.validate = function(data, offset = 0){
        const {errorCount, results} = engine.executeOnText(data);
        
        results.forEach(result => result.messages.forEach(msg => msg.line += offset));
        
        grunt.log.writeln(formatter(results));
        if (errorCount) throw new Error('aborted by ESLint');
    };
}

Referencing the template URL from your script (example)

Sometimes it can be useful not to hardcode the template path into your script. The following code does the trick:

const path = require('path');
.
.
.
grunt.initConfig({
    sfc: {
        your_target: {
            src: ['*.component'],
            options: {
                processors: {
                    js: function(content){
                        // "$$TEMPLATE_URL$$" will act as a magic constant in your script
                        return content.replace(/$$TEMPLATE_URL$$/g, `'${this.templateUrl}'`);
                    },
                    .
                    .
                    .
                },
                onTranspileStart: (file, nodes) => {
                    const template = findNode('template');
                    if (!template) return;
                    
                    const script = findNode('script');
                    if (!script) return;
                    
                    const {dst, attrs: {dst: originalDst}} = template;
                    
                    script.templateUrl = isFile(originalDst)
                        ? originalDst
                        // ".posix" is necessary for proper separating
                        : path.posix.join(originalDst, getFileName(dst));
                    
                    function isFile(file)      {return !!path.parse(file).ext;}
                    function getFileName(file) {return path.parse(file).base;}
                    function findNode(name)    {return nodes.find(node => node.name === name);}
                }
            }
        }
    }
});

Referencing the template content from your script (example)

For example in AngularJS the template can be included in the js file. To achieve this we compile the template first and then we interpolate it back into the script:

dummy.ng:

<!-- 
  Since the order is matter, we have to put the template on the first place so the compiled  
  HTML will be accessible to the following processors.
-->
<template lang="pug" dst="<%= project.dirs.tmp %>/views/">
.foo
  .bar XxX
</template>

<script lang="js" dst="<%= project.dirs.dist %>/scripts/">
console.log($$TEMPLATE$$);
</script>
.
.
.

gruntfile.js:

grunt.initConfig({
    sfc: {
        your_target: {
            src: ['*.ng'],
            options: {
                processors: {
                    pug: require('pug').render,
                    js: function(content){
                        const template = grunt.file.read(this.templatePath);
                        return content.replace('$$TEMPLATE$$', `'${template}'`);;
                    },
                    .
                    .
                    .
                },                
                onTranspileStart: (file, nodes) => {
                    const template = findNode('template');
                    if (!template) return;
                    
                    const script = findNode('script');
                    if (!script) return;
                    
                    script.templatePath = template.dst;
                    
                    function findNode(name) {return nodes.find(node => node.name === name);}
                }
            }
        }
    }
});

Sample processors

In this section you can see a set of sample processors. They are not intended to have their own repository, I just aim them to be a good starting point.

pug-processor.js:

(function(module, require) {
const pug = require('pug');

module.exports = function pugProcessorFactory({basedir, scope}) {
    return Object.assign(function pugProcessor(src) {
        src = '\n'.repeat(this.nodeStart) + src; // line number fix
        return pug.compile(src, {basedir, pretty: true})(scope);
    }, {id: 'pug', ext: '.html'});
};
    
})(module, require);

sass-processor.js:

(function(module, require) {
const sass = require('node-sass');

module.exports = function sassProcessorFactory({basedir}) {
    return Object.assign(function sassProcessor(src) {
        try {
            src = '\n'.repeat(this.nodeStart) + src; // line number fix
            return sass.renderSync({
                data: src,
                indentedSyntax: true,
                includePaths: [basedir],
                outputStyle: 'compressed'
            }).css;
        } catch(e) {
            throw new Error(e.formatted); // throw the proper message
        }
    }, {id: 'sass', ext: '.css'});
};

})(module, require);

js-processor.js:

(function(module, require) {
const
    path   = require('path'),
    eslint = require('./eslintcli'); // check the implementation out before

module.exports = function jsProcessorFactory({scope = {}}) {
    return Object.assign(jsProcessor, {
        id: 'js',
        ext: '.js',
        onTranspileStart
    });
    
    function jsProcessor(src) {
        const
            scp = Object.assign({}, scope, {TEMPLATE_URL: `'${this.templateUrl}'`}),
            result = src.replace(/\$\$(\w+)\$\$/g, (match, id) => id in scp ? scp[id] : match);

        eslint.validate(result, this.nodeStart);
        return result;
    }

    function onTranspileStart(file, nodes) {
        const template = findNode('template');
        if (!template) return;
    
        const script = findNode('script');
        if (!script) return;
    
        const {dst, attrs: {dst: originalDst}} = template;
    
        script.templateUrl = isFile(originalDst)
            ? originalDst
            // ".posix" is necessary for proper separating
            : path.posix.join(originalDst, getFileName(dst));
    
        function findNode(name)    {return nodes.find(node => node.name === name);}
        function isFile(file)      {return !!path.parse(file).ext;}
        function getFileName(file) {return path.parse(file).base;}
    }
};

})(module, require);

Release History

  • 0.0.1: Initial release
  • 0.0.2: Fixed file naming issue
  • 0.0.3: Improved performance
  • 0.0.4: Fixed node parsing issue
  • 0.0.5: Small improvements
  • 0.0.6: User defined file extensions (exts) are now merged with the defaults
  • 0.0.7:
    1. Fixed line ending issue
    2. Processor context
  • 0.0.8: Added dstBase option
  • 0.0.9: Added onTranspileStart and onTranspileEnd hooks
  • 0.0.10:
    1. Processors can return falsy
    2. More detailed readme
  • 0.0.11: dst is back
  • 0.0.12: Fixed missing grunt.template.process() call
  • 0.0.13: Added quiet option
  • 0.0.14: onTranspileStart and onTranspileEnd can be func[]
  • 0.0.15:
    1. Sample processors
    2. Empty nodes are skipped
  • 0.0.16:
    1. You can suppress dstBase by using absolute path
    2. Treat empty dst as a valid path
  • 0.0.17: Processors can be queried
  • 0.0.18: Small refactor
  • 0.0.19:
    1. processor attribute is DEPRECATED use lang instead
    2. lang and processor attributes are not mandatory anymore
    3. Async processor support
  • 0.0.20: Fixed error on task loading
  • 0.0.21:
    1. Nodes can contain empty attributes (e.g. scoped)
    2. processor.ext does not have to be dot prefixed