I'm trying to move one of my custom elements into a plug-in so that I can re-use it across projects.
I had a look at the skeleton plugin and noticed that it has a src/index.js that returns a config with all custom elements defined as globalResources.
So I tried the same thing and I basically have:
src/index.js
export function configure (config) {
config.globalResources([
'./google-map',
'./google-map-location-picker',
'./google-map-autocomplete'
]);
}
And then I have each one of my custom elements next to index.js, for example:
google-map.js
import {inject, bindable, bindingMode, inlineView} from 'aurelia-framework';
#inlineView(`
<template>
<div class="google-map"></div>
</template>
`)
#inject(Element)
export class GoogleMapCustomElement {
// All the Custom Element code here
}
I've also set up a basic npm script that runs babel on the code and sticks it in dist/:
"main": "dist/index.js",
"babel": {
"sourceMap": true,
"moduleIds": false,
"comments": false,
"compact": false,
"code": true,
"presets": [ "es2015-loose", "stage-1"],
"plugins": [
"syntax-flow",
"transform-decorators-legacy",
"transform-flow-strip-types"
]
},
"scripts": {
"build": "babel src -d dist"
},
Tbh I'm not entirely sure this is all correct but I took some of it from the skeleton plugin and it seems to run fine.
Anyway, the problem I'm having is that after I install the plugin (npm install --save-dev powerbuoy/AureliaGoogleMaps), add it to my aurelia.json in build.bundles[vendor-bundle.js].dependencies and tell aurelia to use it in main.js (.use.plugin('aurelia-google-maps')) I get:
GET http://localhost:9000/node_modules/aurelia-google-maps/dist/index/google-map.js (404)
So my question is, where does it get the dist/index/ part from?? I'm configuring my globalResources in index.js but nowhere does it say that I have an index folder.
What am I doing wrong?
Bonus question: What is the bare minimum required to transpile my ES6 plug-in code so that others can use it? Does my babel configuration look correct?
What about referencing your plugin within aurelia.json, like this:
{
"name": "aurelia-google-maps",
"path": "../node_modules/aurelia-google-maps/dist",
"main": "index"
}
I have absolutely no idea why, but in order to solve this problem I actually had to move my custom elements inside an index/ folder.
So now I have this:
- index.js
- index/
- custom-element-one.js
- custom-element-two.js
And my index.js still looks like this:
export function configure (config) {
config.globalResources([
'./custom-element-one',
'./custom-element-two'
]);
}
Where it gets index/ from I guess I will never know, but this works at least.
I did need the babel plug-in Marton mentioned too, but that alone did not solve the mystery of the made up path.
Edit: To elaborate a bit further, if I name my main entry point something other than index.js the folder too needs that name. For example, if I were to rename index.js main.js I would need to put my globalResources inside a folder called main/.
Update:
Edit: thanks for clarifying why you don't want to use the whole skeleton-plugin package.
Focusing on your original question: aurelia-cli uses RequireJS (AMD format) to load dependencies. Probably, your current output has a different format.
Add transform-es2015-modules-amd to babel.plugins to ensure AMD-style output, so it will be compatible with RequireJS and therefore with aurelia-cli.
"babel": {
"sourceMap": true,
"moduleIds": false,
"comments": false,
"compact": false,
"code": true,
"presets": [ "es2015-loose", "stage-1"],
"plugins": [
"syntax-flow",
"transform-decorators-legacy",
"transform-flow-strip-types",
"transform-es2015-modules-amd"
]
}
Original:
There are several blog post about plugin creation, I started with this: http://patrickwalters.net/making-out-first-plugin/ .
Of course, there have been many changes since then, but it's a useful piece of information and most of it still applies.
I'd recommend using plugin-skeleton as project structure. It provides you with a working set of gulp, babel, multiple output formats out-of-the-box.
With this approach, your plugin's availability wouldn't be limited to JSPM or CLI only but everyone would have the possibility to install it regardless of their build systems.
Migration is fairly easy in your case:
Download skeleton-plugin
Copy your classes + index.js into src/
npm install
...wait for it...
gulp build
check dist/ folder
most of your pain should now be gone :)
Here are some details based on my observations/experience.
1. Main index.js/plugin-name.js:
In general, a main/entry point is required, where the plugin's configure() method is placed. It serves as a starting point when using it within an Aurelia application. This file could have any name, usually it's index.js or plugin-name.js (e.g. aurelia-google-maps.js) to make it clear for other developers what should be included for bundling. Set that same entry point in package.json as well.
In addition to globalResources, you can implement a callback function to allow configuration overrides. That can be called in the application, which will use the plugin. Example solution
Plugin's index.js
export * from './some-element';
export function configure(config, callback) {
// default apiKey
let pluginConfig = Container.instance.get(CustomConfigClass);
pluginConfig.apiKey = '01010101';
// here comes an override
if (callback) {
callback(pluginConfig);
}
...
config.globalResources(
'./some-element'
);
}
Your app's main.js
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('aurelia-google-maps', (pluginConfig) => {
// custom apiKey
pluginConfig.apiKey = '12345678';
});
aurelia.start().then(a => a.setRoot());
}
2. HTML and CSS resources:
If you have html only custom elements, you can make them available using globalResources.
Custom css styling is going to require a bit of additional configuration in bundling configuration (see below).
3. Using the plugin with aurelia-cli: Documentation
One of the first new features you'll see soon is a command to help you with 3rd party module configuration. The command will inspect a previously npm-installed package, and make a configuration recommendation to you, automating the process if you desire.
While we are looking forward to that above moment, let's edit aurelia.json:
Configure plugin dependencies. If there are any external libraries (e.g. Bootstrap), then those should be included before your plugin.
Include your plugin:
...
{
"name": "plugin-name",
"path": "../node_modules/plugin-name/dist/amd",
"main": "plugin-name",
"resources": ["**/*.html", "**/*.css"] // if there is any
},
...
Now, your plugin is ready to include it in main.js as showed in Section 1..
I hope you didn't get sick of reading the word 'plugin' so many (21!) times. :D
Related
For the example, everything seems to be working with chrome debugger extension installed.
When I tried to use:
<script lang="ts">
the source map seems just mess up. Following the instructions here, I can debug in Chrome, but not in vscode anymore.
I used the version 3 vue-cli template with TypeScript in Vue.js Single-File-Components rather than the guide you have linked, but I had a similar problem.
The vue-cli v.3 template ends up outputting TypeScript components' sourcemaps into a '.' folder (but all under webpack://) while the JavaScript components' sourcemaps end up in a 'src' folder. This resulted in the default sourceMapPaths working for the JavaScript SFCs but not the TypeScript SFCs. Therefore, I could set breakpoints in Chrome debugger directly, but not in the original files in VSCode for TypeScript SFCs.
My solution was to correct the mapping via the sourceMapPathOverrides configuration (alternatively, it could be corrected by modifying the build process but this seemed like the simple approach).
In .vscode/launch.json, you can set the appropriate mappings. The config I used ended up looking similar to the following (but you may have to adjust based on your exact setup):
{
"type": "chrome",
"request": "launch",
"name": "ChromeDebug",
"url": "http://localhost:8080",
"webRoot": "${workspaceFolder}",
"breakOnLoad": true,
"sourceMaps": true,
"disableNetworkCache": true,
"sourceMapPathOverrides": {
"webpack:///*": "${webRoot}/*",
"webpack:///./*": "${webRoot}/*",
"webpack:///src/*": "${webRoot}/src/*"
}
}
(The last sourceMapPathOverrides entry is probably redundant. Your link seems to be using a '.://' root output path rather than my 'webpack://' root and you may also need to adjust the webRoot path to wherever your source files are. Don't forget to change the url/port as well, if it differs from my example.)
...
And the following is probably not related to OP's issue, but may be helpful for someone with a similar problem:
When using TypeScript, ensure that you have "sourceMap": true in your tsconfig.json.
If using vue-cli v.3, you may also need to add a vue.config.js file in the project root, such as the following, to change the devtool value from the default:
module.exports = { configureWebpack: { devtool: "source-map" } };
I am having hard times wrapping my mind around new CLI and configuration.
In the official documentation, I couldn't really find anything about CSS and how to add it as an entry point and not import it directly into an component or main.js file.
I realized that some JS files are being chunked into separate file, from main.js and the rest gets compiled where supposed to - into the app.js.
I was wondering, how does it know in the background what should be stored as "vendor" for the JS, but when I try to import some "vendor" SASS files into main.js it does not and it merges all within a single app.css file.
Can anyone tell me, how does one create/modify the vue.config.js and tell the bundler that I also want app.scss to be an entry point and vendor.scss to be another entry point.
I am unsure what are best practices for such purpose, but I always did it this way with my own webpack config...
Partial example below:
entry: {
vendor: [
'./styles/vendor.scss',
'./scripts/vendor.js'
],
app: [
'./styles/app.scss',
'./scripts/app.js'
]
}
EDIT #1
I think I got the first one...
"How does it know what should be chunked in "vendor" files?
Whatever gets imported from node_modules, it is being chunked.
What I did not figure out yet is... What if I am having my personal assets/styles/vendor directory where I #import those SASS files from NPM and do some modifications of variables or whatever.
Importing this file to main.js does not get chunked in this case... So there must be a way to tell bundler that I want everything within that directory or everything within vendor.scss file where everything is being imported, to be chunked out.
EDIT #2
I figured I can use WebPack's magical comments to import the main vendor SCSS file, such as:
import(/* webpackChunkName: "vendor" */ './assets/styles/vendor.scss')
I don't have a problem with this, but apparently the bundler does. It generates an empty vendor.[hash].js file as well.
EDIT #3
I did further research and learned that there's a command vue inspect which would output the webpack configuration.
So when making tweaks to vue.config.js, we can look a the output with this command if there's a bug or something is not working as expected.
Further more, I learned that if we specify entry directly in our vue.config.js file, that we will get an error that entry cannot be specified within our configuration file.
The following is forbidden to do so, but it's what I actually want to achieve...
// vue.config.js
module.exports = {
entry: {
app: [
'./src/main.js',
'./src/assets/styles/app.scss'
],
vendor: [
'./src/assets/styles/vendor.scss'
]
}
}
The actual proper way to do this will be an answer to my own question...
The way to achieve this is by using WebPack's Chain API.
However, if I did everything correctly, I still see a problem of generated vendor.[hash].js file with some WebPack module boilerplate. This JS file is also being injected to the index.html template.
Which leads to the same outcome as the attempt of my EDIT #2, except that we're no longer importing our Sass files within main.js
To modify entry points for my purpose of this question, we can do it the following way:
// vue.config.js
module.exports = {
chainWebpack: config => {
config
.entry('app')
.add('./src/assets/styles/app.scss')
.end()
.entry('vendor')
.add('./src/assets/styles/vendor.scss')
.end()
}
}
Note
We're not specifying the app entry JS file, which would be main.js by default, because we're not overriding the current entry point. Instead, we're extending it, so everything works as expected.
UPDATE
Until WebPack resolves this in future major releases, I found a great package - fqborges/webpack-fix-style-only-entries. It solves this issue that I was having and I'd suggest you to use it.
Final configuration would look like this:
const FixStyleOnlyEntries = require('webpack-fix-style-only-entries')
module.exports = {
chainWebpack: config => {
config
.entry('app')
.add('./src/assets/styles/app.scss')
.end()
.entry('vendor')
.add('./src/assets/styles/vendor.scss')
.end()
},
configureWebpack: {
plugins: [
new FixStyleOnlyEntries()
]
}
}
UPDATE #2
After further investigation and use of such configuration for projects, I realized that I had to use !important in styles where I had a need to override anything vendor related.
This is simply because WebPack will inject app, before vendor (both JS and CSS) and it will cause such issue.
Even if we modify the configuration from above and move app entry, below the vendor entry, it will still fail. Reason being, because we're modifying the entry point which already exists by default within vue-cli config. We're adding more entries to the app and we're adding new vendor entry.
To fix this issue of ordering, we must delete the app entirely and then create it ourselves.
const FixStyleOnlyEntries = require('webpack-fix-style-only-entries')
module.exports = {
chainWebpack: config => {
config.entryPoints.delete('app')
config
.entry('vendor')
.add('./src/assets/styles/vendor.scss')
.end()
.entry('app')
.add('./src/main.js')
.add('./src/assets/styles/app.scss')
.end()
},
configureWebpack: {
plugins: [
new FixStyleOnlyEntries()
]
}
}
I'm building a web (react with webpack & babel) and mobile apps (react-native with expo) for a project. I therefore created a common library for business logic and redux/api library.
Some code will be slightly different between web and mobile. In my case it's localStorage vs AsyncStorage, which I use for authentication among other things...
I'm trying to pass an environment variable for the build stage to switch import of certain files so that the correct file is loaded for each build which are simply path linked (ie no pre-build of my library, I just do import '../mylib') ex:
if(PLATFORM === 'mobile'){
import StorageModule from './mobile-storage-module`
} else {
import StorageModule from './mobile-storage-module`
}
export default StorageModule
Try 1
#babel/preset-env to say if it's mobile or web so that it imports different libraries depending on build like so:
My .babelrc has this:
{
"presets": [
[
"#babel/preset-env",
{
"platform": "mobile"
}
]
]
}
And then in local storage file I do this:
export default () => {
const platform = process.env.platform
if (platform === 'mobile') {
return import './storage-modules/storage-mobile'
}
return import './storage-modules/storage-web'
}
That didn't work, and this also didn't work for me.
Try 2
I installed react-native-dotenv and created a .env file with:
PLATFORM=mobile
And set the plugin in my .babelrc:
{
"presets": [
"babel-preset-expo",
"react-native-dotenv"
]
}
And in my example file, I tried this:
import { PLATFORM } from 'react-native-dotenv'
export default PLATFORM === 'mobile' ? import './storage-modules/storage-mobile' : import './storage-modules/storage-web'
But now my build doesn't work. Any idea how I do dynamic imports during the build process that works for babel in react-native app and webpack build (also uses babel)?
First, #babel/preset-env does not do what you think it does. This is not for specifying your own variables, it is a plugin to automatically use the right target and pollyfills for the browsers you want to support.
The easiest way to get environment variables is with the webpack define plugin (which is part of webpack, so no need to install anything extra)
Just add this to your webpack config.
plugins: [
new webpack.DefinePlugin({
'process.env': {
platform: 'mobile',
},
}),
],
Next, you can't use normal import statements inside of ifs.
import gets resolved before any code runs, either on build by webpack, or in supported environments on script load.
To import something on runtime, you need to use dynamic imports.
Here is an example of how this could look like.
export default new Promise(async resolve => {
resolve(
process.env.platform === 'mobile'
? (await import('./mobile.js')).default
: (await import('./desktop.js')).default
);
});
You can now import from this file like you normally would, but be aware that the default export is a promise.
As your question's title says "during babel build phase", I assume you would like to make different builds for desktop and mobile (not one build for both and load the needed modules dynamically run-time). So I would go like this:
Define the run scripts in package.json for desktop and mobile:
"scripts": {
"devmobile": "cross-env NODE_ENV=development PLATFORM=mobile webpack --progress",
"dev": "cross-env NODE_ENV=development webpack --progress",
}
... or you can create two different webpack.config.js files for desktop and mobile builds but I think the above is easier...
Then npm run devmobile to build for mobile and npm run dev for desktop.
Since I'm on Windows I use the cross-env package but this is the recommended way to be OS independent.
Then I would use Webpack's NormalModuleReplacementPlugin:
(based on this exmaple)
In your webpack.config.js:
// defining the wanted platform for the build (comfing form the npm run script)
const targetPlatform = process.env.PLATFORM || 'desktop';
// then use the plugin like this
plugins: [
new webpack.NormalModuleReplacementPlugin(/(.*)-PLATFORM(\.*)/, function(resource) {
resource.request = resource.request.replace(/-PLATFORM/, `-${targetPlatform}`);
}),
]
...then if you have these two files:
./storage-modules/storage-mobile.js
./storage-modules/storage-desktop.js
import the needed one in your script like this:
import './storage-modules/storage-PLATFORM';
This way the generated build will only contain the needed file for the current PLATFORM used for the build process.
Another possible solution could be the ifdef-loader but I haven't tested it. Maybe worth to try, seems easy.
If you want one build though and import the needed module dynamically, you could do something like this in your app.js (or whatever):
// this needs to have defined when the app is running
const targetPlatform = process.env.PLATFORM || 'desktop';
import(
/* webpackChunkName: "[request]" */
`./storage-modules/storage-${targetPlatform}`
).then(storageModule => {
// use the loaded module
});
or:
(async () => {
const storageModule = await import(
/* webpackChunkName: "[request]" */
`./storage-modules/storage-${targetPlatform}`
);
// use the loaded module
})();
For this to work Babel has to be configured.
More on Webpack with dynamic imports here.
You can use transform-inline-environment-variablesto pass platform to babel
"build-mobile": "PLATFORM=mobile ...",
"build-app": "PLATFORM=app ...",
I am building an Aurelia app with TypeScript and decided to try out Semantic UI. I followed this question (Aurelia Semantic dropdown) and it helped me install Semantic into Aurelia. It seems that it got installed already built with default theme. Is there a way I can install semantic into Aurelia TypeScript app, then add some custom gulp tasks to build according to my own theme.config? I would like also to override some variables like colors, font sizes etc. After it is built I'd like to use the built version in Aurelia view models (TypeScript) and in my views. How can I achieve that?
Here is how I solved this:
Installed semantic to some local folder with npm
Copied over the semantic folder and semantic.json to web app root folder (so semantic folder is on the level where I have node_modules and jspm_packages)
Inside semantic.json I specified the list of components I want to include in my app
Inside semantic.json I modified "output" and "clean" paths to match the folders where I serve files from.
The semantic.json:
{
"base": "semantic",
"paths": {
"source": {
"config": "src/theme.config",
"definitions": "src/definitions/",
"site": "src/site/",
"themes": "src/themes/"
},
"output": {
"packaged": "../dist/semantic",
"uncompressed": "../dist/semantic/components/",
"compressed": "../dist/semantic/components/",
"themes": "../dist/semantic/themes/"
},
"clean": "../dist/semantic"
},
"permission": false,
"autoInstall": false,
"rtl": false,
"version": "2.2.4",
"components": [
"button",
...
"site"
]
}
Inside Aurelia's gulp definitions I added semantic build task
The build/tasks/build.js:
var buildSemantic = require('../../semantic/tasks/build');
gulp.task('build-semantic', buildSemantic);
...
gulp.task('build-layout', function (callback) {
return runSequence(
'build-html',
'build-semantic',
'build-less',
callback
);
});
When coding I go into semantic src (e.g. semantic\src\themes\default\globals\site.variables) and modify things in there
Run gulp build-layout
The output is added to my dist folder and I can use it in my views
As for the view models I created some helper components to be used as aurelia attributes e.g. the semantic tooltip:
import {customAttribute, inject} from 'aurelia-framework';
import * as $ from 'jquery';
import '../semantic/semantic.min.js';
#customAttribute('semantic-tooltip')
#inject(Element)
export class SemanticTooltip {
constructor(private element: HTMLElement) {
}
attached() {
$(this.element).popup();
}
}
Usage:
<i class="info circle icon" data-content="Sample tooltip" semantic-tooltip></i>
I develop an iPad/iPhone App web app. Both share some of the resources. Now I wanna build a bootstrap js that looks like this:
requirejs(['app'], function(app) {
app.start();
});
The app resource should be ipadApp.js or iphoneApp.js. So I create the following build file for the optimizer:
{
"appDir": "../develop",
"baseUrl": "./javascripts",
"dir": "../public",
"modules": [
{
"name": "bootstrap",
"out": "bootstrap-ipad.js",
"override": {
"paths": {
"app": "ipadApp"
}
}
},
{
"name": "bootstrap",
"out": "bootstrap-iphone.js",
"override": {
"paths": {
"app": "iphoneApp"
}
}
}
]
}
But this doesn't seems to work. It works with just one module but not with the same module with different outputs.
The only other solution that came in my mind was 4 build files which seems a bit odd. So is there a solution where i only need one build file?
AFAIK the r.js optimizer can only output a module with a given name once - in your case you are attempting to generate the module named bootstrap twice. The author of require.js, #jrburke made the following comment on a related issue here:
...right now you would need to generate a separate build command for each script being targeted, since the name property would always be "almond.js" for each one.
He also suggests:
...if you wanted just one build file to run, you could create a node program and drive the optimizer multiple times in one script file. This example shows using requirejs as a module and calling requirejs.optimize().
I took a similar approach in one of my projects - I made my build.js file an ERB template and created a Thor task that ran through my modules and ran r.js once for each one. But #jrburke's solution using node.js is cleaner.