I'm fairly new with Javascript/Typescript and was developing a Single-Spa with Single SPA Router
Searching for info about registering applications I found different ways of doing that, one way is using the LayoutEngine that "automatically" register my applications this way:
const routes = constructRoutes(microfrontendLayout);
const applications = constructApplications({routes, loadApp({name}) {
return System.import(name);
}});
const layoutEngine = constructLayoutEngine({ routes, applications });
applications.forEach(registerApplication);
layoutEngine.activate();
From what I understood is, in microfrontend-layout.html we have to put something like an HTML tag in this way
<application name="#prueba-organizacion/micro-prueba-app1"></application>
with that and having an importmap in index.ejs the app was loading fine.
But then I found another way, the manually way:
A)
import { registerApplication, start } from "single-spa";
registerApplication({
name: '#prueba-organizacion/micro-prueba-app1',
app: () => System.import('#prueba-organizacion/micro-prueba-app1'),
activeWhen: (location) => location.pathname.startsWith('/app1')
});
start();
But that says: Promise\<System.Module\> is not assignable to type Application\<{}\>
Then I found ANOTHER WAY of doing the same as above:
B)
System.import('single-spa').then(({ registerApplication, start }) => {
registerApplication({
name: '#prueba-organizacion/micro-prueba-app1',
app: () => System.import('#prueba-organizacion/micro-prueba-app1'),
activeWhen: (location) => location.pathname.startsWith('/app1'),
});
start();
});
The thing here is, what's the difference between point A) and point B)?
I mean, they're importing the modules and registerApplication, start functions but one of them is throwing me an error.
I'm using "single-spa": "^5.9.3" from package.json
Thanks!
I've tried both ways, one of them is not working, the other one is working (B) as expected
Related
I have a relatively simple requirement to start out with.
I want to be able to load a particular UI component, based on some dynamic context. I was thinking that dynamic imports could be used for this purpose. The dynamic imports should be loaded through the Web browser (no nodeJS).
Details about what is possible with dynamic imports are very sketchy to me - at best, probably also because I'm not an expert in the field of JavaScript/TypeScript (yet).
I'm using vanilla JS at the moment and vitejs as build tool.
Here is what I have so far.
This is my main.ts file:
const getPath = () => {
if (import.meta.env.MODE === 'development') {
return 'http://127.0.0.1:3000/one.js';
} else {
return 'http://127.0.0.1:5000/one.js';
}
};
import(getPath()).then((Module) => {
Module.default();
});
This already makes vitejs barf, complaining that it cannot analyze things - but I'm ignoring that for now. The one.js file looks like this:
const hello = () => {
console.log('Hello from one');
};
export default hello;
Both run dev as run serve work with this setup, as-in, the message is printed on the browser console.
My next thing I want to see working is how one.js would be able to itself import a module and work with that. I've tried this with moment like so:
import moment from "moment";
const hello = () => {
console.log('Hello from one ', moment().format());
};
export default hello;
This errors with:
Uncaught (in promise) TypeError: Error resolving module specifier “moment”. Relative module specifiers must start with “./”, “../” or “/”.
Now I don't know whether what I want to do, is not support or that I'm just not doing it the right way. Any pointers would be appreciated.
I have a blog run on Gatsby, and every time I push and deploy new blog posts I need to do a refresh on my page to see the new data.
I tried following the suggestions from this post and adding an onServiceWorkerUpdate function but that doesn't seem to have done anything.
Anyone have workarounds to this issue, and if so will there be a way to test locally? Changes already automatically update when I test in gatsby develop mode.
This is the entirety of my gatsby-browser.js file
export const onServiceWorkerUpdateReady = () => window.location.reload();
You need to install gatsby-plugin-offline first. Leaving your gatsby-config.js with something similar to:
{
plugins: [
{
resolve: `gatsby-plugin-manifest`,
options: {
...
}
},
'gatsby-plugin-offline'
]
}
Note: plugin's order matters in this case.
The plugin will register a service worker and will load it into the client.
Then, in your gatsby-browser.js file you can simply add:
export const onServiceWorkerUpdateReady = () => {
const answer = window.confirm(
`This application has been updated. ` +
`Reload to display the latest version?`
)
if (answer === true) window.location.reload()
}
Additionally, you may want to add the following hack (very common across the repositories):
export const onRouteUpdate = () => {
navigator.serviceWorker.register('/sw.js').then((reg) => {
reg.update();
});
};
Basically, it forces the registration of the service-worker across the site upon the onServiceWorkerUpdateReady refresh.
Check this interesting thread for further caveats and workarounds for some specific scenarios: https://github.com/gatsbyjs/gatsby/issues/9087#issuecomment-774680408
It will not work using Link provided by gatsby
import { Link as GatsbyLink } from 'gatsby';
<GatsbyLink to={to} {...props} />
It works using traditional a tag:
<a href={props.to} target="_blank">
{props.children}
</a>
I'm trying to make an improvement to an existing Gatsby plug-in, and I want to pass a React Component to the plug-in, through its configuration entry in gatsby-config.js:
plugins: [
{
resolve: `gatsby-plugin-modal-routing`,
options: { someComponent: SomeComponentClassOrFunction }
},
However, the problem I'm running into is that I can't figure out how to make it work.
If I try to pass the component itself as part of the plug-in's configuration, it seems to get serialized to/from JSON, resulting in the class becoming a useless object. So it seems I have to pass a path string instead.
plugins: [
{
resolve: `gatsby-plugin-modal-routing`,
options: {
modalComponentPath: path.join(__dirname, 'src/components/SomeComponent.js')
}
},
However, if I try to pass the path instead, I can't figure out how to use it to load the component inside the plug-in. I've tried using a dynamic Node import (ie. import(path).then(component => ...)) ...
with a path that's path.join-ed with __dirname
with a relative path (src/components/SomeComponent)
with a local-path-relative path (./src/components/SomeComponent)
with and without a trailing .js
I'm not sure if this is some sort of issue with the different paths of the app vs. the plug-in or whether there's some other problem, but using import seems like an un-Gatsby-like solution anyway.
So, then I discovered the loadPage and loadPageSync functions which are passed into the plug-in ... but those failed also. Every path I try results in component coming back ... but it's a "page not found" component (presumably because the component I'm trying to pass in hasn't been added as a page).
This seems like it should be a simple question, at least to anyone who has worked on Gatsby plug-ins before: if you want a plug-in to take a component as an input (either as a function/class or as a string of a path to a module) ... how can you actually use that component in your plug-in?
All I'm looking for is a basic pattern or reference to a line in an existing Gatsby plugin that takes a component, or something simple like that (I can look up any details).
This seems like it should be a simple question
I had the same thought while trying this out myself. Oh boy.
TL:DR
// gatsby-node.js
const { DefinePlugin } = require('webpack')
const path = require('path')
exports.onCreateWebpackConfig = ({ actions }, { componentPath }) => {
actions.setWebpackConfig({
plugins: [
new DefinePlugin({
'___COMPONENT___': JSON.stringify(componentPath)
})
]
})
}
// gatsby-ssr
export const onRenderBody = ({ setPreBodyComponents }) => {
const Component = require(___COMPONENT___).default
setPreBodyComponents([<Component />])
}
Long read
Gatsby config doesn't seem to pass functions around (I could have sworn it used to), so passing a React component directly to your custom plugin is out the window. It has to be a path to your component.
// gatsby-config.js
{
resolve: 'my-custom-plugin',
options: {
componentPath: path.join(__dirname, './my-component.js')
}
}
You didn't say if you're using the component in gatsby-node or gatsby-browser/ssr, but I assume it's the later since requiring stuff dynamically in Node is dead simple:
Gatsby Node
// gatsby-node.js
function consume(component) {
const Component = require(component)
}
...although it doesn't understand JSX or ESM, but that's a different problem.
Gatsby Browser
gatsby-browser/ssr is run with webpack, so the module format is not a problem. But import(componentPath) won't work:
Dynamic expressions in import()
It is not possible to use a fully dynamic import statement, such as import(foo). Because foo could potentially be any path to any file in your system or project.
webpack doc
Ok, I suppose so something like this should work:
// gatsby-browser
import('./my-dir' + componentPath)
Nope, because webpack will try to resolve this from wherever the plugin live, i.e node_modules or plugins directory & we're not about to ask our users to put their custom components in node_modules.
What about this, then?
// gatsby-browser
import(process.cwd() + componentPath) // nope
We're right back at the beginning — webpack doesn't like full dynamic path! And also even if this works, this is a terrible idea since webpack will try to bundle the whole working directory.
Only if we could encode the path as a static string beforehand, so webpack can just read that code — like using webpack.DefinePlugin to define environment variables. Fortunately we can do that in gatsby-node.js:
// gatsby-node.js
const { DefinePlugin } = require('webpack')
const path = require('path')
exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
plugins: [
new DefinePlugin({
'___CURRENT_DIR___': JSON.stringify(process.cwd())
})
]
})
}
And finally
// gatsby-browser
// eslint throw error for unknown var, so disable it
// eslint-disable-next-line
import(___CURRENT_DIR___ + componentPath) // works, but don't do this
But since we can access user options right in gatsby-node, let's just encode the whole path:
// gatsby-node.js
const { DefinePlugin } = require('webpack')
- const path = require('path')
- exports.onCreateWebpackConfig = ({ actions }) => {
+ exports.onCreateWebpackConfig = ({ actions }, { componentPath }) => {
actions.setWebpackConfig({
plugins: [
new DefinePlugin({
- '___CURRENT_DIR___': JSON.stringify(process.cwd())
+ '___COMPONENT___': JSON.stringify(componentPath)
})
]
})
}
Back in gatsby-browser.js:
// gatsby-browser
// I pick a random API to test, can't imagine why one would import a module in this API
export const onRouteUpdate = async () => {
// eslint-disable-next-line
const { default: Component } = await import(___COMPONENT___)
console.log(Component) // works
}
Gatsby SSR
For the sake of completeness, let's try the same trick in gatby-ssr:
// gatsby-ssr
export const onRenderBody = async ({ setPreBodyComponents }) => {
// const Component = require(___COMPONENT___).default
const { default: Component } = await import(___COMPONENT___)
setPreBodyComponents([<Component />])
}
...and it failed.
Why? If one's curious enough they might go and dig around Gatsby code to see how gatsby-ssr is treated differently than gatsby-browser, but alas I just don't feel like doing that.
Fear not, we still have one trick up our sleeve. Webpack's require can import module dynamically too, though not asynchronously. Since gatsby-ssr doesn't run in the browser, I couldn't care less about asynchronicity.
export const onRenderBody = ({ setPreBodyComponents }) => {
const Component = require(___COMPONENT___).default
setPreBodyComponents([<Component />]) // works
}
And now it works.
Sharing code between gatsby-ssr & gatsby-browser
Let's just say we need this component in both gatsby-ssr and gatsby-browser — would require(...) works in gatsby-browser too?
export const onRouteUpdate = async () => {
// eslint-disable-next-line
const { default: Component } = require(___COMPONENT___)
console.log(Component) // yes
}
It works.
import(..) vs require()
While import() does load stuff dynamically, it is more of a tool for code-splitting. Here's some different, other than asynchronicity:
using import('./my-dir' + componentPath) will bundle all files inside ./my-dir into a chunk. There's magic comment we can use to exclude/include stuff.
require(...) will just inline the required component into whatever chunk's calling it.
In the getServerSideProps function of my index page, I'd like to use a function foo, imported from another local file, which is dependent on a certain Node library.
Said library can't be run in the browser, as it depends on "server-only" modules such as fs or request.
I've been using the following pattern, but would like to optimize it. Defining foo as mutable in order to have it be in scope is clunky and seems avoidable.
let foo;
if (typeof window === "undefined") {
foo = require("../clients/foo");
}
export default function Index({data}) {
...
}
export async function getServerSideProps() {
return {
props: {data: await foo()},
}
}
What would be the best practice here? Is it somehow possible to leverage ES6's dynamic import function? What about dynamically importing within getServerSideProps?
I'm using Next.js version 9.3.6.
Thanks.
UPDATE:
It seems as if Next.js's own dynamic import solution is the answer to this. I'm still testing it and will update this post accordingly, when done. The docs seem quite confusing to me as they mentionn disabling imports for SSR, but not vice versa.
https://nextjs.org/docs/advanced-features/dynamic-import
When using getServerSideProps/getStaticProps, Next.js will automatically delete any code inside those functions, and imports used exclusively by them from the client bundle. There's no risk of running server code on the browser.
However, there are a couple of considerations to take in order to ensure the code elimination works as intended.
Don't use imports meant for the server-side inside client-side code (like React components).
Ensure you don't have unused imports in those files. Next.js won't be able to tell if an import is only meant for the server, and will include it in both the server and client bundles.
You can use the Next.js Code Elimination tool to verify what gets bundled for the client-side. You'll notice that getServerSideProps/getStaticProps gets removed as do the imports used by it.
Outside of getServerSideProps/getStaticProps, I found 2 fairly similar solutions.
Rely on dead code elimination
In next.config.js:
config.plugins.push(
new webpack.DefinePlugin({
'process.env.RUNTIME_ENV': JSON.stringify(isServer ? 'server' : 'browser'),
}),
);
export const addBreadcrumb = (...params: AddBreadcrumbParams) => {
if (process.env.RUNTIME_ENV === 'server') {
return import('./sentryServer').then(({ addBreadcrumb }) => addBreadcrumb(...params));
}
return SentryBrowser.addBreadcrumb(...params);
};
Note that some for reason I don't understand, dead code elimination does not work well if you use async await, or if you use a variable to store the result of process.env.RUNTIME_ENV === 'server'. I created a discussion in nextjs github.
Tell webpack to ignore it
In next.config.js
if (!isServer) {
config.plugins.push(
new webpack.IgnorePlugin({
resourceRegExp: /sentryServer$/,
}),
);
}
In that case you need to make sure you will never import this file in the client otherwise you would get an error at runtime.
You can import the third party library or a serverside file inside getServerSideProps or getInitialProps since these functions run on server.
In my case I am using winston logger which runs on server only so importing the config file only on server like this
export async function getServerSideProps (){
const logger = await import('../logger');
logger.info(`Info Log ->> ${JSON.stringify(err)}`);
}
You can also import library/file which has default export like this
export async function getServerSideProps(context) {
const moment = (await import('moment')).default(); //default method is to access default export
return {
date: moment.format('dddd D MMMM YYYY'),
}
}
Because it's become a commonly used pattern to put markups, styling and javascript of 1 specific React component into 1 single file. Is it also a good pattern to put the unit tests (for example mocha describes) to that same file?
Note: Let's assume the bundle tool can strip unused imports and the codes inside describes.
Example:
import React from 'react';
import { shallow } from 'enzyme';
import { expect } from 'chai';
const MyComponent = ({ active }) => (
<div style={{ color: active ? 'blue' : 'gray' }}>
{active ? 'this is active' : 'this is inactive'}
</div>
);
export default MyComponent;
// mocha tests (same file)
describe('MyComponent', () => {
context('active = true', () => {
it('should include is active text', () => { ... });
it('should have color blue', () => { ... });
});
context('active = false', () => { ... });
});
This would bind you and will lead to many problems. I do not know much about your architecture. But in this way you are not allowed to use any analyze tool. If you will be using any tool to test your Code Climate for example SonarQube it will be parsing also test and it fails.
Another scenario: Let's assume that you are running necessary builds when you are pushing to the repository. Your app is growing and you need to run SonarQube within each pull. You need to add a heuristic check if your pull is touching *.spec.js files -> again you can not.
This is also not used and not best practice. The most used and most common practice is to put your spec to the file with the same name and postfix spec.js or to the different folder within the root of component like __spec__/component.js.
Your approach will not help to anything at all and you can not separate pure code from tests in any scenario which is very bad.