Less, Coffee, Sass, bundle e outros no Visual Studio 2015


Se você é como eu e tem necessidade de testar toda e qualquer nova tecnologia, entende “beta” como “estável o suficiente para mim”, usa mais plugins do que programas propriamente ditos e estava assistindo à Build desse ano, é muito provável que você tenha decidido testar o Visual Studio 2015RC e todas as novas fantásticas features do .NET 5, é muito provável também que você tenha ficado feliz em ver que o WebEssentials já foi atualizado para o 2015RC, ai você instalou e… Pera ai, por que o Less não compila? Por que o Bundle não gera? Por que os scripts não minificam? A resposta é simples: Esses recursos foram removidos do WebEssentials!

Pode ser um choque nos primeiros 15 minutos, a praticidade do WebEssentials nesse ponto era realmente impressionante, mas depois que a gente se acalma começa a perceber que faz algum sentido.

A Microsoft vem investindo pesado no Azure e nos recursos de Build no server, inclusive recentemente lançou uma versão do Visual Studio para Mac e Linux baseada nesse recurso.

O WebEssentials trabalhava basicamente client-side no Visual Studio e só conseguia compilar o front-end quem tivesse ele na máquina, alem de terem que manter o compilador de less, sass, coffee etc, sendo que já existem outras ferramentas para isso.

Ta, legal, mas e ai? Como eu fico?

Existem algumas ferramentas para substituir essas funções e o WebEssentials agora te ajuda a configura-las.

Minha escolha pessoal foi o Grunt, mas muita gente prefere o Gulp, ambos são compiladores de script bastante poderosos e existe ainda no pacote o Bower, que é gerenciador de pacotes de script. Assim como o NuGet se encarrega de manter os pacotes de código do seu backend atualizados (como Entity Framework, Ajax Toolkit etc) o Bower é mais focado no seu front (Angular, Bootstrap, Jquery etc).

Eu sei que as libs que eu dei de exemplo existem no NuGet também, mas em geral, a recomendação é usar o Nuget para Assemblies .Net e o Bower para CSS e JS.

Tanto o Bower quanto o Grunt são baseados em Node.js, então antes de utiliza-los você vai precisar instalar o Node.js.

Assumo que você já instalou o WebEssentials.

(você provavelmente vai precisar de um reboot para o comando npm ser reconhecido).

Clique no projeto com o botão direito, e em Add haverá uma nova opção: “Grunt and Bower to Project”.

Add Grunt

 

Serão criados três arquivos com uma estruta JSON, o bower.json, gruntfile.js e package.json. No momento não nos preocuparemos com o Bower, queremos apenas substituir o WE.

No arquivo Package você precisa adicionar os pacotes com as extensões que usará dentro de “devDependencies”. O intelisense funciona para os pacotes e ajuda bastante, alguns podem não ter um nome muito obvio, nada que um rápido google não resolva, mas segue o meu exemplo com o que estou usando nesse momento enquanto configuro um projeto que estou migrando para o 2015RC:

{
  "name": "package",
  "version": "1.0.0",
  "private": true,
  "devDependencies": {
    "grunt": "0.4.5", // grunt
    "grunt-contrib-less": "^1.0.1", // Compilador Less
    "grunt-contrib-cssmin": "^0.12.3", // Minificador CSS
    "grunt-contrib-copy": "^0.8.0", //Copia de arquivo
    "grunt-contrib-clean": "^0.6.0", //Exclusão de arquivos
    "grunt-contrib-uglify": "^0.9.1", // Minificação de JS
    "grunt-contrib-watch": "^0.6.1" // monitora arquivos para build automático
  }
}

(se copiar remova os comentários, o npm não aceita comentários)

Criado o arquivo de configuração de pacotes agora é preciso instala-los, abra um prompt e navegue até a pasta do seu projeto, com o comando npm install (nome do pacote) é possivel instalar cada pacote.

npm install grunt
npm install grunt-contrib-less
npm install grunt-contrib-cssmin
npm install grunt-contrib-copy
npm install grunt-contrib-clean
npm install grunt-contrib-uglify
npm install grunt-contrib-watch

Pronto para uso

Com as libs instaladas agora é “só” configurar o Build.

O arquivo gruntfile.js é criado com a configuração do bower, que eu vou apagar do meu exemplo por não estar utilizando, você vai entender a estrutura de cada job utilizando as libs, mas em geral os jobs tem mais ou menos esse formato:

modulo: {
	nomedojob:{
		opcoes:
			{ ... },
		arquivos:{
			'destino':'origem'
		}
	}
}

a grande vantagem de escrever esse arquivo de configuração, na minha opnião, é poder ter mais de um perfil de Build, é possível ter um perfil de produção e outro de testes no qual não se minifica os arquivos para facilitar o debug.

segue, para fins ilustrativos o arquivo de configuração que eu estou usando agora, pode não ter a sintaxe ideal ou talvez de pra otimizar, mas está quebrando o meu galho.

module.exports = function (grunt) {
    grunt.initConfig({
        less: {
            development: {
                files: {
                    "App_Themes/Cadastro/css/cadastro.css": "App_Themes/Cadastro/less/Cadastro.less",
                    "App_Themes/Evento/css/evento.css": "App_Themes/Evento/less/Evento.less",
                    "App_Themes/Home/css/home.css": "App_Themes/Home/less/Home.less",
                    "App_Themes/Principal/css/componentes.css": "App_Themes/Principal/less/Componentes.less",
                    "App_Themes/Principal/css/goinout.css": "App_Themes/Principal/less/Goinout.less",
                    "App_Themes/TermosDeUso/css/style.css": "App_Themes/TermosDeUso/less/style.less",
                }
            }
        },
        copy: {
            main: {
                files: [
                    { expand: true, flatten: true, dest: "App_Themes/Cadastro/css/", src: ["App_Themes/Cadastro/unprocessed/*"], filter: 'isFile' },
                    { expand: true, flatten: true, dest: "App_Themes/Evento/css/", src: ["App_Themes/Evento/unprocessed/*"], filter: 'isFile' },
                    { expand: true, flatten: true, dest: "App_Themes/Home/css/", src: ["App_Themes/Home/unprocessed/*"], filter: 'isFile' },
                    { expand: true, flatten: true, dest: "App_Themes/Principal/css/", src: ["App_Themes/Principal/unprocessed/*"], filter: 'isFile' },
                    { expand: true, flatten: true, dest: "App_Themes/TermosDeUso/css/", src: ["App_Themes/TermosDeUso/unprocessed/*"], filter: 'isFile' }
                ]
            }
        },
        cssmin: {
            target: {
                files: {
                    'App_Themes/Cadastro/css/cadastro.min.css': ['App_Themes/Cadastro/css/*.css'],
                    'App_Themes/Evento/css/evento.min.css': ['App_Themes/Evento/css/*.css'],
                    'App_Themes/Home/css/home.min.css': ['App_Themes/Home/css/*.css'],
                    'App_Themes/Principal/css/goinout.min.css': ['App_Themes/Principal/css/*.css'],
                    'App_Themes/TermosDeUso/css/style.min.css': ['App_Themes/TermosDeUso/css/*.css']
                }
            }
        },
        clean: {
            pre: {
                src: ["App_Themes/**/css/*", "Script/minified/*"]
            },
            pos: {
                src: ["App_Themes/**/css/*", "!App_Themes/**/css/*.min.css"]
            }
        },
        uglify: {
            all: {

                files: {
                    'Scripts/minified/facebook.js': ['Scripts/Facebook.js'],
                    'Scripts/minified/home.js': ['Scripts/Home.js'],
                    'Scripts/minified/mapstyle.js': ['Scripts/mapstyle.js'],
                    'Scripts/minified/eventolink.js': ['Scripts/EventoLink.js'],
                    'Scripts/minified/addfriends.js': ['Scripts/AddFriends.js'],
                }
            }
        },
        watch: {
            files: ["App_Themes/**/less/*.less", "Scripts/*.js"],
            tasks: ["default"]
        }
    });

    grunt.loadNpmTasks("grunt-contrib-less");
    grunt.loadNpmTasks("grunt-contrib-copy");
    grunt.loadNpmTasks("grunt-contrib-clean");
    grunt.loadNpmTasks("grunt-contrib-cssmin");
    grunt.loadNpmTasks("grunt-contrib-watch");
    grunt.loadNpmTasks("grunt-contrib-uglify");

    grunt.registerTask("default", ["clean:pre","less","copy","cssmin","uglify","clean:pos"]);
};

Repare que eu descrevo as tasks, importo as libs com o grunt.loadNpmTasks(”); e depois defino um alias “default” para uma sequencia de tasks.

Repare também que eu tenho dois jobs de clean no alias default, e escolho qual usar com “:”.

Ao abrir o Task Runner Explorer podemos ver e executar as tasks:

TraskRunner

 

Ta brincando né? como eu automatizo isso?

Há duas formas de automatização aqui, a mais simples está no próprio Task Runner, clicando com o botão direito você vai perceber que existe a opção de criar Binds para a abertura do projeto, pré compilação, pós compilação e clean:

Binds

A outra está na task Watch, que monitora a edição de arquivos, atribuindo um Bind de “Project Open” ela será executada ao abrir o projeto e ficará monitorando a lista de arquivos que você especificou para cada tasks, o que gera um comportamento muito parecido com o que o WebEssentials fazia, você pode especificar que os arquivos .less acionarão o compilador de less ou os .js o minificador de JavaScript, eu prefiro executar todo o processo sempre que algo muda:

        watch: {
            files: ["App_Themes/**/less/*.less", "Scripts/*.js"],
            tasks: ["default"]
        }

TL;DR;

O compilador do WebEssentials foi aposentado em favor de ferramentas melhores mais flexíveis, depois de configurado uma vez, o comportamento do projeto é bem parecido, mas em todo início de projeto você vai perder uns minutos configurando o processo de compilação do seu front-end, não é tão prático quanto era mas é o padrão que está sendo adotado pelo Visual Studio.