Webpackowe horrory

Macie wrażenie, że na początku każdego bardziej złożonego projektu zamiast zabrać się do soczystego pisania kodu siedzicie i wpisujecie kolejne linie pluginów oraz loaderów? Czasem uda się podebrać część configa z jakiegoś repozytorium a czasem nie. Obsługa dekoratorów, wyłączenie przetwarzania zmiennej x, a po skomplikowaniu... Okazało się, że o czymś zapomnieliście.

Dla pocieszenia: z czasem jest łatwiej, a dobrze przygotowane środowisko developerskie pozwala na szybkie postępy na dalszym etapie prac. Dlatego dzisiaj odwdzięczam się społeczności za całe wsparcie aż dwoma configami. Bierzcie i jedzcie z tego wszyscy!

Wspólna baza konfiguracyjna

To oczywiście plik webpacka dla fullstackowego projektu, który wspólnie piszemy :)

Nie jest tajemnicą, że źródło wyjściowe z webpacka będzie wyglądało zupełnie różnie dla back-endu oraz front-endu. Nie znaczy to jednak, że musimy tworzyć parę plików.

Po prostu w jednym pliku webpack.config.js wystawimy dwie konfiguracje, które będziemy rozpoznawać po nazwie. Jest to standardowa praktyka wspierana przez webpacka.

Zacznijmy jeszcze od części wspólnej.

Jeszcze nie myślę o wersji produkcyjnej, więc ustawiam tryb na development, który w webpacku 4 domyślnie wprowadza większość popularnych opcji związanych z optymalizacją. Dodatkowo zawsze włączam watcha (uruchamianie procesu po każdym zapisie), ponieważ w trakcie developmentu prawie nie korzystam z tradycyjnego builda. To jest osobista preferencja :)

const config = {
    mode: 'development',
    watch: true,
};

Zapraszamy na front

Tutaj dodajemy standardową obsługę Vue z TypeScriptem oraz transpilację Babela. Dodatkowo przetwarzanie SCSS oraz CSS, chociaż prawdopodobnie będę korzystała tylko z SCSS to jest to bardzo pomocne dla tego przykładu.

Name

Szczególnie należy zwrócić uwagę na wartość name, bo to po niej będziemy odwoływać się do konkretnego configa w jednym pliku, jeśli - jak w tym przypadku - wystawiamy dwa w jednym pliku.

VueLoaderPlugin

Pozwala na obsługę jednoplikowych kompontentów Vue (taki zawierających style, skrypt oraz html) bez żadnych kombinacji. Jeśli w komponencie Vue umieścisz style, webpack zastosuje do nich wszystkie reguły dla styli zdefiniowane w konfiguracji. Tutaj załączam link z większość ilością informacji: VueLoaderPluginDocs.

TsconfigPathsPlugin

Jak sama nazwa wskazuje odpowiada za czytanie ścieżek zdefiniowanych w pliku konfiguracyjnym tsconfig.json.

Target

Target służy do transpilacji na konkretną platformę: zmieni to na przykład importy w wynikowych plikach.

const frontend = {
    ...config,
    name: 'frontend',
    entry: {
        'app': path.resolve(__dirname, 'src', 'app', 'app.ts'),
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'src', 'public'),
    },
    plugins: [
        new VueLoaderPlugin()
    ],
    resolve: {
        extensions: ['.ts', '.js', '.vue', '.json'],
        plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json" })],
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
        }
    },
    target: 'web',
    module: {
        rules: [{
                test: /\.ts$/,
                loader: 'ts-loader',
                exclude: /node_modules/,
                options: {
                    appendTsSuffixTo: [/\.vue$/],
                }
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader',
                options: {
                    loaders: {
                        'scss': 'vue-style-loader!css-loader!sass-loader',
                        'sass': 'vue-style-loader!css-loader!sass-loader?indentedSyntax',
                    }
                }
            },
            {
                include: [path.resolve(__dirname)],
                loader: 'babel-loader',
                test: /\.js$/,
                exclude: /node_modules/
            },
            {
                test: /\.(scss|css)$/,

                use: [{
                        loader: 'style-loader'
                    },
                    {
                        loader: 'css-loader'
                    },
                    {
                        loader: 'sass-loader'
                    }
                ]
            }
        ]
    },
};

Backendowo jeszcze ciekawiej

Poza standardowymi deklaracjami oraz tymi opisanymi wcześniej, mamy jeszcze kilak ciekawych:

node

W właściwości node będziemy w stanie nadpisać/udostępnić/wyłączyć niektóre globalne zmienne pochodzące z nodejs, żeby nie psuły nam builda. Do popularnych rozwiązań należy wyłączenie fs w przypadku niektórych bibliotek czy też - tak jak tutaj - mockowanie __dirname.

externals

To taki wyłącznik dla bundlowania należności. Jeśli masz bibliotekę x, która korzysta z y to czasem chcesz załączyć y, a czasem zostawiasz to użytkownikowi: bo może już tą bibliotekę ma załadowaną.

Podobnie jest w przypadku NodeJS, który sam potrafi importować, więc pakowanie tego do jednego pliku jest bezsensowne. Dodatkowo pomaga nam plugin nodeExternals, który sprawia, że nie musimy listować nic ręcznie.

Podsumowanie

Teraz wystarczy wyeksportować oba configi w tablicy:

//webpack.config.js
module.exports = [frontend, backend];

oraz napisać w npm (lub yarnie) skróty

    "scripts": {
        "watch1": "webpack --config ./webpack.config.js --config-name frontend",
        "watch2": "webpack --config ./webpack.config.js --config-name backend",
        "watch3": "nodemon  --inspect -r esm --watch api/api.js"
    },

Ostatni to nodemon, który będzie obserwował plik wynikowy i restartował node'a w razie zmian, ale do tego kiedyś przejdziemy :)

Bardzo Wam dziękuję za uwagę, mam nadzieje, że odwdzięczyłam się za lata kopiowania configów i w końcu dołożyłam cegiełkę do tego domu w którym wszyscy żyjemy :)

Oto repo: https://github.com/KamilaBrylewska/StoryPortal

Jeszcze tam grzebię z konfiguracjami, ale możecie sobie zobaczyć cały kod w przyjemniejszej wersji i ściągnąć do siebie.

A poniżej ts.config:

 {
    "compilerOptions": {
        "baseUrl":"./", 
        "outDir":"./dist/",
		"moduleResolution": "node",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "sourceMap": true,
        "module": "commonjs",
        "target":"es5",
        "allowJs":true, 
        "emitDecoratorMetadata":true, 
        "experimentalDecorators":true, 
        "allowSyntheticDefaultImports":true, 
        "lib":[
            "es7", 
            "es2015", 
            "dom"
        ], 
        "paths": {
            "Components/*": [
                "src/components/*"
            ],
            "Entity/*": [
                "src/entity/*"
            ],
            "*":["types/*"],
        }
    },
    "include": [
        "./src/**/*"
    ], 
    "exclude":[
        "node_modules", 
        "**/*.spec.ts"
    ]
}

oraz babelRC:

 {
     "plugins": [
         "@babel/plugin-syntax-dynamic-import",
         "dynamic-import-node-babel-7", ["@babel/plugin-proposal-decorators", { "legacy": true }],
         ["@babel/plugin-transform-modules-commonjs", { "allowTopLevelThis": true, "strict": false, "loose": true }],
         ["@babel/plugin-transform-regenerator", {
             "asyncGenerators": true,
             "generators": true,
             "async": true
         }],
         [
             "@babel/plugin-transform-runtime", {
                 "corejs": false,
                 "helpers": true,
                 "regenerator": true,
                 "useESModules": true
             }
         ],
         "@babel/plugin-proposal-class-properties",
         "@babel/plugin-syntax-object-rest-spread"
     ],

     "presets": [
         ["@babel/preset-env", { "useBuiltIns": false }],
         "@babel/typescript"
     ]
 }

Kamila

Dziękuję za poświęcony czas, będzie mi bardzo miło jak zostawisz komentarz :)