I started to write library to fetch some data from an api in typescript and use webpack as a bundler.
We have a dev, test and prod api, so the lib should use differnt urls for each environment.
Webpack has the normalModuleReplacmentPlugin build in, to replace files, depending on the configuration. I created different environment files, which should be replaced by webpack.
I'm using Ubuntu 18.04, but one of our developer which is now working on the lib currently using windows. He noticed that the replacement is not working on his local machine.
webpack.config.js (example from my github repo)
const merge = require( 'webpack-merge' );
const path = require( 'path' );
const commonConfigObj = {
entry: {
'fooBar': './src/index.ts'
},
output: {
filename: '[name].js',
path: path.resolve( __dirname, 'dist' ),
library: 'FooBar',
libraryTarget: 'umd'
},
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{
loader: 'ts-loader',
options: { configFile: 'tsconfig.json' }
}
]
}
]
},
resolve: {
extensions: [ '.ts', '.tsx', '.js' ]
},
profile: true
};
const commonConfig = merge( [ commonConfigObj ] );
const environments = {
'prod': require( './webpack/prod.config.js' ),
'dev': require( './webpack/dev.config.js' )
};
module.exports = mode=>{
if( mode ) {
const envConfig = environments[ mode.env ];
if( envConfig ) {
return merge( commonConfig, envConfig );
}
}
return merge( commonConfig, environments.dev );
};
webpack/prod.config.js (example from my github repo)
const CleanWebpackPlugin = require( 'clean-webpack-plugin' );
const BundleAnalyzerPlugin = require( 'webpack-bundle-analyzer' ).BundleAnalyzerPlugin;
const webpack = require('webpack');
module.exports = {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new BundleAnalyzerPlugin( {
analyzerMode: 'disabled',
generateStatsFile: true
} ),
new webpack.NormalModuleReplacementPlugin(
/src\/environments\/environment.ts/,
'./environment.prod.ts'
),
],
devtool: 'source-map'
};
I'm sure that this is not a webpack issue but wrong usage.
GitHub repo: https://github.com/ManticSic/normalModuleReplacmentPlugin-issue-windows
Run npm ci and npm run build (or npm run build-dev for dev env).
You can find the important part at the end of line #1
expected: ...,t.environment={bar:"Prod"}...
result on windows: ...,t.environment={bar:"Normal"}...
Like expected the problem was sitting in front of the computer...
Wrong file system separators were used.
Replacing \/ with [\\\/] works fine!
Full regex: /src[\\\/]environments[\\\/]environment.ts/
Related
Working on a new project using Nextjs and Storybook. We are using SCSS modules to style our components, and they work just fine in the actual app on the browser, but they won't link up in the stories themselves. Here are a few simple snippets to show where I'm at right now:
Component:
import React from 'react'
import styles from './VideoEntryTile.module.scss'
const VideoEntryTile: React.FC = () => {
return (
// This displays properly in the browser but not storybook
<div className={styles.container}>
<p>Hello</p>
</div>
)
}
export default VideoEntryTile
SCSS module:
.container {
background-color: blueviolet;
}
Component story:
import React from 'react';
import { ComponentStory, ComponentMeta } from '#storybook/react';
import VideoEntryTile from './VideoEntryTile';
export default {
title: 'Video Entry Tile',
component: VideoEntryTile,
argTypes: {
},
} as ComponentMeta<typeof VideoEntryTile>;
const Template: ComponentStory<typeof VideoEntryTile> = (args) => <VideoEntryTile {...args} />;
export const Primary = Template.bind({});
Primary.args = {};
./storybook/main.js:
module.exports = {
core: {
builder: 'webpack5',
},
"stories": [
"../src/**/*.stories.mdx",
"../src/**/*.stories.#(js|jsx|ts|tsx)",
"../src/components/**/*.stories.tsx"
],
"addons": [
"#storybook/addon-links",
"#storybook/addon-essentials",
{
name: '#storybook/preset-scss',
options: {
sassLoaderOptions: {
modules: true
}
}
}
],
"framework": "#storybook/react"
}
Can anyone tell me where I'm going wrong? Thanks.
Create a file called scss-preset.js in .storybook:
// Copied from https://github.com/storybookjs/presets/blob/master/packages/preset-scss/index.js
function wrapLoader(loader, options) {
if (options === false) {
return [];
}
return [
{
loader,
options,
},
];
}
function webpack(webpackConfig = {}, options = {}) {
const { module = {} } = webpackConfig;
const {
styleLoaderOptions,
cssLoaderOptions,
sassLoaderOptions,
rule = {},
} = options;
return {
...webpackConfig,
module: {
...module,
rules: [
...(module.rules || []),
{
test: /(?<!module)\.s[ca]ss$/,
...rule,
use: [
...wrapLoader('style-loader', styleLoaderOptions),
...wrapLoader('css-loader', {
...cssLoaderOptions,
modules: false,
}),
...wrapLoader('sass-loader', sassLoaderOptions),
],
},
{
test: /\.module\.s[ca]ss$/,
...rule,
use: [
...wrapLoader('style-loader', styleLoaderOptions),
...wrapLoader('css-loader', {
...cssLoaderOptions,
modules: true,
}),
...wrapLoader('sass-loader', sassLoaderOptions),
],
},
],
},
};
}
module.exports = { webpack };
This is going through each scss file and, if it's a module.scss file, processing it differently than the ones that aren't.
in main.js, add presets: [path.resolve("./.storybook/scss-preset.js")], to module.exports. Should be all you need.
Make sure you have installed sass
npm install sass --save
Install scss preset
npm install #storybook/preset-scss --save
Add it to the addons list inside your .storybook/main.js
addons: ['#storybook/preset-scss',]
Imagine I have OrderComponent and CustomerComponent that represent two screens.
In the web version you can create a new customer while in the order screen by launching the customer component inside a popup. So OrderComponent references CustomerComponent directly in the template. So I have to keep both in the same FeaturesModule for this to work.
On the other hand in the Nativescript mobile version, there is no such capability and so the two components/screens are completely independent, so I would like to put them in 2 separate modules: OrderModule, and CustomerModule. And lazy load them so the app launches faster.
In my real application of course it's not just 2 components but several dozens so the mobile app performance is a more pressing issue.
When I try to add a module file with the tns extension like this: order.module.tns.ts, without the corresponding web file, it seems as if the NativeScript bundler is not picking it up, I get the following error:
ERROR: C:\...\src\app\features\orders\orders.module.ts is missing from the TypeScript compilation. Please make sure it is in your tsconfig via the 'files' or 'include' property.
at AngularCompilerPlugin.getCompiledFile (C:\Users\...\node_modules\#ngtools\webpack\src\angular_compiler_plugin.js:753:23)
at plugin.done.then (C:\Users\...\node_modules\#ngtools\webpack\src\loader.js:41:31)
at process._tickCallback (internal/process/next_tick.js:68:7)
# ../$$_lazy_route_resource lazy namespace object ./features/measurement-units/measurement-units.module
# ../node_modules/#angular/core/fesm5/core.js
# ../node_modules/nativescript-angular/platform.js
# ./main.ns.ts
But according to the docs, all I have to do to make a nativescript specific component is add it with the tns extension. Is there another step to make this work for module files?? Help is appreciated
Update
Here is my tsconfig.tns.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "es2015",
"moduleResolution": "node",
"baseUrl": "./",
"paths": {
"~/*": [
"src/*"
],
"*": [
"./node_modules/tns-core-modules/*",
"./node_modules/*"
]
}
}
}
Update 2
My webpack.config.js:
const { join, relative, resolve, sep } = require("path");
const webpack = require("webpack");
const nsWebpack = require("nativescript-dev-webpack");
const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target");
const { nsReplaceBootstrap } = require("nativescript-dev-webpack/transformers/ns-replace-bootstrap");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeScriptWorkerPlugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const { AngularCompilerPlugin } = require("#ngtools/webpack");
module.exports = env => {
// Add your custom Activities, Services and other Android app components here.
const appComponents = [
"tns-core-modules/ui/frame",
"tns-core-modules/ui/frame/activity",
];
const platform = env && (env.android && "android" || env.ios && "ios");
if (!platform) {
throw new Error("You need to provide a target platform!");
}
const projectRoot = __dirname;
// Default destination inside platforms/<platform>/...
const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot));
const appResourcesPlatformDir = platform === "android" ? "Android" : "iOS";
const {
// The 'appPath' and 'appResourcesPath' values are fetched from
// the nsconfig.json configuration file
// when bundling with `tns run android|ios --bundle`.
appPath = "app",
appResourcesPath = "app/App_Resources",
// You can provide the following flags when running 'tns run android|ios'
aot, // --env.aot
snapshot, // --env.snapshot
uglify, // --env.uglify
report, // --env.report
sourceMap, // --env.sourceMap
hmr, // --env.hmr,
} = env;
const externals = (env.externals || []).map((e) => { // --env.externals
return new RegExp(e + ".*");
});
const appFullPath = resolve(projectRoot, appPath);
const appResourcesFullPath = resolve(projectRoot, appResourcesPath);
const entryModule = `${nsWebpack.getEntryModule(appFullPath)}.ts`;
const entryPath = `.${sep}${entryModule}`;
const ngCompilerPlugin = new AngularCompilerPlugin({
hostReplacementPaths: nsWebpack.getResolver([platform, "tns"]),
platformTransformers: aot ? [nsReplaceBootstrap(() => ngCompilerPlugin)] : null,
mainPath: resolve(appPath, entryModule),
tsConfigPath: join(__dirname, "tsconfig.tns.json"),
skipCodeGeneration: !aot,
sourceMap: !!sourceMap,
});
const config = {
mode: uglify ? "production" : "development",
context: appFullPath,
externals,
watchOptions: {
ignored: [
appResourcesFullPath,
// Don't watch hidden files
"**/.*",
]
},
target: nativescriptTarget,
entry: {
bundle: entryPath,
},
output: {
pathinfo: false,
path: dist,
libraryTarget: "commonjs2",
filename: "[name].js",
globalObject: "global",
},
resolve: {
extensions: [".ts", ".js", ".scss", ".css"],
// Resolve {N} system modules from tns-core-modules
modules: [
resolve(__dirname, "node_modules/tns-core-modules"),
resolve(__dirname, "node_modules"),
"node_modules/tns-core-modules",
"node_modules",
],
alias: {
'~': appFullPath
},
symlinks: true
},
resolveLoader: {
symlinks: false
},
node: {
// Disable node shims that conflict with NativeScript
"http": false,
"timers": false,
"setImmediate": false,
"fs": "empty",
"__dirname": false,
},
devtool: sourceMap ? "inline-source-map" : "none",
optimization: {
splitChunks: {
cacheGroups: {
vendor: {
name: "vendor",
chunks: "all",
test: (module, chunks) => {
const moduleName = module.nameForCondition ? module.nameForCondition() : '';
return /[\\/]node_modules[\\/]/.test(moduleName) ||
appComponents.some(comp => comp === moduleName);
},
enforce: true,
},
}
},
minimize: !!uglify,
minimizer: [
new UglifyJsPlugin({
parallel: true,
cache: true,
uglifyOptions: {
output: {
comments: false,
},
compress: {
// The Android SBG has problems parsing the output
// when these options are enabled
'collapse_vars': platform !== "android",
sequences: platform !== "android",
}
}
})
],
},
module: {
rules: [
{
test: new RegExp(entryPath),
use: [
// Require all Android app components
platform === "android" && {
loader: "nativescript-dev-webpack/android-app-components-loader",
options: { modules: appComponents }
},
{
loader: "nativescript-dev-webpack/bundle-config-loader",
options: {
angular: true,
loadCss: !snapshot, // load the application css if in debug mode
}
},
].filter(loader => !!loader)
},
{ test: /\.html$|\.xml$/, use: "raw-loader" },
// tns-core-modules reads the app.css and its imports using css-loader
{
test: /[\/|\\]app\.css$/,
use: {
loader: "css-loader",
options: { minimize: false, url: false },
}
},
{
test: /[\/|\\]app\.scss$/,
use: [
{ loader: "css-loader", options: { minimize: false, url: false } },
"sass-loader"
]
},
// Angular components reference css files and their imports using raw-loader
{ test: /\.css$/, exclude: /[\/|\\]app\.css$/, use: "raw-loader" },
{ test: /\.scss$/, exclude: /[\/|\\]app\.scss$/, use: ["raw-loader", "resolve-url-loader", "sass-loader"] },
{
test: /(?:\.ngfactory\.js|\.ngstyle\.js|\.ts)$/,
use: [
"nativescript-dev-webpack/moduleid-compat-loader",
"#ngtools/webpack",
]
},
// Mark files inside `#angular/core` as using SystemJS style dynamic imports.
// Removing this will cause deprecation warnings to appear.
{
test: /[\/\\]#angular[\/\\]core[\/\\].+\.js$/,
parser: { system: true },
},
],
},
plugins: [
// Define useful constants like TNS_WEBPACK
new webpack.DefinePlugin({
"global.TNS_WEBPACK": "true",
"process": undefined,
}),
// Remove all files from the out dir.
new CleanWebpackPlugin([`${dist}/**/*`]),
// Copy native app resources to out dir.
new CopyWebpackPlugin([
{
from: `${appResourcesFullPath}/${appResourcesPlatformDir}`,
to: `${dist}/App_Resources/${appResourcesPlatformDir}`,
context: projectRoot
},
]),
// Copy assets to out dir. Add your own globs as needed.
new CopyWebpackPlugin([
{ from: "fonts/**" },
{ from: "**/*.jpg" },
{ from: "**/*.png" },
], { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] }),
// Generate a bundle starter script and activate it in package.json
new nsWebpack.GenerateBundleStarterPlugin([
"./vendor",
"./bundle",
]),
// For instructions on how to set up workers with webpack
// check out https://github.com/nativescript/worker-loader
new NativeScriptWorkerPlugin(),
ngCompilerPlugin,
// Does IPC communication with the {N} CLI to notify events when running in watch mode.
new nsWebpack.WatchStateLoggerPlugin(),
],
};
if (report) {
// Generate report files for bundles content
config.plugins.push(new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false,
generateStatsFile: true,
reportFilename: resolve(projectRoot, "report", `report.html`),
statsFilename: resolve(projectRoot, "report", `stats.json`),
}));
}
if (snapshot) {
config.plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({
chunk: "vendor",
angular: true,
requireModules: [
"reflect-metadata",
"#angular/platform-browser",
"#angular/core",
"#angular/common",
"#angular/router",
"nativescript-angular/platform-static",
"nativescript-angular/router",
],
projectRoot,
webpackConfig: config,
}));
}
if (hmr) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
}
return config;
};
This is my first time build a webpack.production.config.js file. Here is my directory structure.
My webpack dev config entry and output is as follows:
const path = require('path');
const webpack = require('webpack');
let config = {
entry: path.resolve(__dirname, 'app/index.js'),
output: {
path: path.resolve(__dirname, '/public'),
filename: 'bundle.js',
},
....
My webpack production file is this:
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: path.resolve(__dirname, 'app/index.js'),
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js',
},
module: {
loaders: [
{ test: /\.json$/, loader: 'json' },
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' },
{
test: /\.css$/,
use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: 'css-loader' }),
},
{ test: /\.(png|svg|jpg|gif)$/, use: ['file-loader'] },
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader'],
},
],
},
plugins: [
new HtmlWebpackPlugin({ title: 'My App', filename: 'admin.html' }),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
new webpack.optimize.UglifyJsPlugin(),
new ExtractTextPlugin('styles.css'),
new webpack.optimize.CommonsChunkPlugin('common.js'),
],
};
Npm start works fine. Npm run build compiles and creates my build folder. In the folder are my assets, a main.js, common.js.js, and admin.html generated from the HtmlWebpackPlugin.
When built, the admin.html is reading the commmon.js.js and main.js sources.
As I try to deploy to heroku, How to get my output paths in production correct so the new generated html is in the root and reads the proper js files? Currently in my package.json, the entry point is "app/index.js".
Currently, I use webpack to build a single JS file which represents my app. How do I split my React app UI shell from rest of the app logic, so that I can have the service Worker cache it?
My webpackpack config file looks like this, that generates a single index_bundle.js file(no css file):
import webpack from 'webpack'
import path from 'path'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import autoprefixer from 'autoprefixer'
const LAUNCH_COMMAND = process.env.npm_lifecycle_event
const isProduction = LAUNCH_COMMAND === 'production'
process.env.BABEL_ENV = LAUNCH_COMMAND
const PATHS = {
root: path.join(__dirname),
app: path.join(__dirname, 'app'),
build: path.join(__dirname, 'dist')
}
const HTMLWebpackPluginConfig = new HtmlWebpackPlugin({
template: PATHS.app + '/index.html',
filename: 'index.html',
inject: 'body'
})
const productionPlugin = new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
})
const productionPlugin2 = new webpack.optimize.UglifyJsPlugin({
compressor: {
warnings: false
}
})
const base = {
entry: [
'babel-polyfill',
PATHS.app
],
output: {
path: PATHS.build,
filename: 'index_bundle.js'
},
module: {
loaders: [
{test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader'},
{test: /\.css$/, loader: 'style!css?sourceMap&modules&localIdentName=[name]__[local]___[hash:base64:5]&importLoader=1!postcss'}
]
},
postcss: [ autoprefixer({ browsers: ['last 2 versions'] }) ],
resolve: {
root: path.resolve('./app')
}
}
const developmentConfig = {
devtool: 'cheap-module-inline-source-map',
devServer: {
contentBase: PATHS.build,
historyApiFallback: true,
hot: true,
inline: true,
progress: true
},
plugins: [HTMLWebpackPluginConfig, new webpack.HotModuleReplacementPlugin()]
}
const productionConfig = {
devtool: 'cheap-module-source-map',
plugins: [HTMLWebpackPluginConfig, productionPlugin, productionPlugin2]
}
export default Object.assign({}, base, isProduction === true ? productionConfig : developmentConfig)
My "Instant Loading with Service Workers" talk from the Chrome Dev Summit 2015 covers creating a PWA using the App Shell + dynamic model, powered by React.
The code sample for it is part of the sw-precache library's repo: https://github.com/GoogleChrome/sw-precache/tree/master/app-shell-demo
(It's not necessarily the most idiomatic React code in the world, but the general concepts, especially when it comes to the service worker implementation, should hold.)
I'm trying to follow some tutorials and documentation and have the webpack build for me sass files into separates css files.
It all kind of works, as long as I'm proving full relative path in require:
require("../sass/ss.scss")
But it I want to use:
require("./ss.scss")
and I turn comment out the 'sassLoader' in the config, I get error:
[1] "sassLoader" is not allowed
As you can see I have been trying to use inline settings too:
sourceMap&includePaths[]=' + (PATHS.sass)
but they are ignored.
What am I doing wrong?
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const validate = require('webpack-validator');
const merge = require('webpack-merge');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PATHS = {
app: path.join(__dirname, 'app'),
js: path.join(__dirname, 'app', 'js'),
sass: path.join(__dirname, 'app', 'sass'),
build: path.join(__dirname, 'build')
};
const common = {
// Entry accepts a path or an object of entries.
// We'll be using the latter form given it's
// convenient with more complex configurations.
entry: {
app: path.join(PATHS.js, 'index.js')
},
output: {
path: PATHS.build,
filename: '[name].js'
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack demo'
}),
new ExtractTextPlugin(
'[name].css', {
allChunks: true
}
),
],
devtool: "source-map",
module: {
loaders: [
{
test: /\.scss$/,
loader: ExtractTextPlugin.extract(
'style!css?sourceMap!sass?sourceMap&includePaths[]=' + (PATHS.sass)
)
}
]
}
// sassLoader: {
// includePaths: [PATHS.sass]
// }
};
var config;
// Detect how npm is run and branch based on that
switch(process.env.npm_lifecycle_event) {
case 'build':
config = merge(common,
{}
);
break;
default:
config = merge(common,
{}
);
}
module.exports = validate(config);
The error comes from webpack-validator because it doesn't recognize the sassLoader property. You need to configure a schema extension:
const Joi = require('webpack-validator').Joi
const schemaExtension = Joi.object({
sassLoader: Joi.any(),
})
module.exports = validate(config, {schemaExtension: schemaExtension})
Requiring CSS modules is handled by the css-loader, only #import is handled by sass-loader. (There are major differences in their behaviour other than that, make sure you are using what you need).
In order to require from the root directory, you need to configure resolve.root in the webpack config file. See answer here: Resolving require paths with webpack