How to publish Next.js app as npm package? - javascript

I have a web app built on Next.js. I want to publish this app as npm package so it can be used in other projects.
I tried to find resources and help using google but did not find any useful information.
Is it possible? if yes how can I achieve this?
Thanks

I have almost the same need. I created a blog using next js pages and i want to share these pages for 2 next js projects.
Until now, i've been able to make it using vite / rollup builder this way (simplified):
NPM package:
// node_modules/#namespace/next-blog/pages/ssr/BlogArticle.tsx
export SSRBlogArticle = getServerSideProps(...) {...}
export BlogArticlePage: NextPage = (SSRProps) => <Blog {..props} />
Using Package in my main next js app
// ./src/pages/blog.tsx
import { SSRBlogArticle, BlogArticlePage } from '#namespace/next-blog'
export getServerSideProps = SSRBlogArticle
const BlogPage: NextPageWithLayout = BlogArticlePage
// some layout
BlogPage.getLayout = (page) => <Layout>{page}</Layout>
export default BlogPage
The problem is about the usage of process.ENV and useRouter. next/link seems not to work...
Here is my vite configuration file :
import { defineConfig } from 'vite'
import react from '#vitejs/plugin-react'
import dts from 'vite-plugin-dts'
import gql from 'vite-plugin-simple-gql'
import tsconfigPaths from 'vite-tsconfig-paths'
import * as path from 'path'
import pkg from './package.json'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
tsconfigPaths(),
gql(),
dts({
insertTypesEntry: true,
}),
],
resolve: {
alias: [{ find: '#', replacement: path.resolve(__dirname, 'src') }],
},
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'NextBlog',
formats: ['es', 'cjs'],
fileName: (format) => `index.${format}.js`,
},
rollupOptions: {
external: [
...Object.keys(pkg.dependencies),
...Object.keys(pkg.peerDependencies),
],
},
},
})
So i would like to add more questions to the original one :
Can we read env file from npm packages? process.ENV reading main app env file ?
Why useRouter is not working properly?
Am i doing it right? Is it good practice?
Thanks for your help :)
Edit
Process.env
I've managed to find out why all process.env were removed from build, vitejs removes them and replace with {}. Solution is :
define: {
// keep process.env* vars in code bundle
'process.env': 'process.env',
},
useRouter
Still impossible to understand the issue with it...
Here one example error on useRouter in main app when clicking on next/link:
Uncaught TypeError: Cannot read properties of null (reading 'push')
at linkClicked (index.es.js?4bcb:3731:1)
at onClick (index.es.js?4bcb:3830:1)
at HTMLUnknownElement.callCallback (react-dom.development.js?ac89:4161:1)
at Object.invokeGuardedCallbackDev (react-dom.development.js?ac89:4210:1)
at invokeGuardedCallback (react-dom.development.js?ac89:4274:1)
at invokeGuardedCallbackAndCatchFirstError (react-dom.development.js?ac89:4288:1)
at executeDispatch (react-dom.development.js?ac89:9038:1)
at processDispatchQueueItemsInOrder (react-dom.development.js?ac89:9070:1)
at processDispatchQueue (react-dom.development.js?ac89:9083:1)
at dispatchEventsForPlugins (react-dom.development.js?ac89:9094:1)
at eval (react-dom.development.js?ac89:9285:1)
at batchedUpdates$1 (react-dom.development.js?ac89:26096:1)
at batchedUpdates (react-dom.development.js?ac89:3988:1)
at dispatchEventForPluginEventSystem (react-dom.development.js?ac89:9284:1)
at dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay (react-dom.development.js?ac89:6462:1)
at dispatchEvent (react-dom.development.js?ac89:6454:1)
at dispatchDiscreteEvent (react-dom.development.js?ac89:6427:1)
For now, the only work around i'm thinking off is using native a tags...
Edit 2
I have finally found out the solution : to make router object not null inside of my package, i have to pass it from main nextjs app to page's package. Inside of the package, i had to wrap my components with RouterContext:
// package next page
import { RouterContext } from 'next/dist/shared/lib/router-context' // next 12
const MyPackagePage = (props) => {
<RouterContext.Provider value={props.router}>
<MyComponents ... />
</RouterContext.Provider>
}
// main next app
const Page = (props) => {
const router = useRouter()
return MyPackagePage({ router, ...props })
}
Now works fine. Less easy to integrate and more boilerplate code, but at least it's possible de export next pages in npm packages.

You can use semantic-release npm package.
In your package.json file you should add follow configuration
"release": {
"branches": [
"master"
],
"plugins": [
"#semantic-release/commit-analyzer",
"#semantic-release/release-notes-generator",
[
"#semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
"#semantic-release/npm",
"#semantic-release/github",
[
"#semantic-release/git",
{
"assets": [
"CHANGELOG.md",
"package.json",
"package-lock.json"
]
}
]
]
}
And your project should be public and should have "private": false in package.json.

Related

How to tell Vite to exclude a subset of files in a directory from build?

I created a new Vue app using npm create vue. During runtime this app fetches a configuration and reads a string from it. This string represents the name of a component to render inside the app. Those dynamic components live inside a "pluggable" directory
.
└── src
├── App.vue
└── pluggables
├── ThisFoo.vue
└── ThatBar.vue
So basically what the App.vue file does is
<script setup lang="ts">
import { onMounted, shallowRef, defineAsyncComponent } from "vue";
const pluggableComponent = shallowRef();
onMounted(() => {
// fetch configuration
const componentName = "ThisFoo"; // extract from configuration
pluggableComponent.value = defineAsyncComponent(() => import(`./pluggables/${componentName}.vue`));
});
</script>
<template>
<div>Pluggable below:</div>
<component :is="pluggableComponent" />
</template>
I have access to the configuration file during build time and know which components I need during runtime and which ones to consider as "dead code" based on this configuration. Is there a way to tell Vite to exclude the unused components from the build?
E.g. exclude the whole pluggables directory but include the required components from the pluggables directory
vite build --exclude ./src/pluggables/** --include ./src/pluggables/ThisFoo.vue
or by creating a custom Vite build function I can call during CI/CD and pass in an array of component names.
To exclude some files from the build process you can mark them as external files by using the external config of Rollup
// vite.config.js
import { defineConfig } from "vite";
import vue from "#vitejs/plugin-vue";
import * as path from "path";
import { fileURLToPath } from "node:url";
const filesNeedToExclude = ["src/pluggables/Comp1.vue", "src/pluggables/Comp2.vue"];
const filesPathToExclude = filesNeedToExclude.map((src) => {
return fileURLToPath(new URL(src, import.meta.url));
});
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"#": path.resolve(__dirname, "./src"),
},
},
build: {
manifest: true,
rollupOptions: {
external: [
...filesPathToExclude
],
},
},
});
Based on Duannx answer I came up with the following solution to exclude everything in the directory except the desired components
import { readdirSync } from 'node:fs'
import { join } from 'node:path'
import { defineConfig } from 'vite'
import vue from '#vitejs/plugin-vue'
function getPluggablesToExclude(): string[] {
const rawPluggablesToInclude = process.env.PLUGGABLES; // !! set this env variable in the CI pipeline !!
if (!rawPluggablesToInclude) { // if missing, exclude nothing
return [];
}
const pluggablesToInclude = rawPluggablesToInclude.split(',').map(component => `${component}.vue`);
const pluggablesDirectoryPath = join(__dirname, 'src', 'pluggables');
const filesInPluggablesDirectory = readdirSync(pluggablesDirectoryPath);
const filesToExclude = filesInPluggablesDirectory.filter(file => !pluggablesToInclude.includes(file));
return filesToExclude.map(file => join(pluggablesDirectoryPath, file));
}
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
external: [
...getPluggablesToExclude()
],
},
},
})
So all I have to do is to set the env variable in my CI pipeline. If I skip this step, the build will include every component.

Unexpected undefined while import with Webpack

I have a problem that has never happened to me before: I'm compiling a little basic starter browser web app (with React) using Webpack + Babel 7.
I've got three different file:
withAuth.js The Auth High Order Component
NavBar.js The NavBar Component
Login.js The Login Form
If I import the withAuth HOC in the NavBar is everything alright, but if I import the withAuth component in the Login.js file it return undefined
/** withAuth.js */
console.log('withAuth Loaded');
const withAuth = Child => ChildProps => (
<AuthContext.Consumer>
{ authClient => <Child {...ChildProps} authClient={authClient} }
</AuthContext.Consumer>
)
export { withAuth };
/** NavBar.js */
import { withAuth } from 'HOC/Auth';
console.log('NavBar Loaded', withAuth); // <- My HOC
const NavBarComponent = (authClient) => { /* ... My Code ... */ }
const NavBar = withAuth(NavBarComponent);
export default NavBar;
/** Login.js */
import { withAuth } from 'HOC/Auth';
console.log('Login Loaded', withAuth); // <- undefined ??
const LoginFormComponent = (authClient) => { /* ... My Code ... */ }
const LoginForm = withAuth(LoginFormComponent);
// /|\
// |
// Will produce an Error, withAuth is Undefined
This is my Webpack Configuration:
/** webpack.config.js */
module.exports = {
entry: { core: 'index.js' },
resolve: {
alias: {
HOC: './path/to/hoc/folder'
}
},
optimization: {
runtimeChunk: 'single',
splitChunks: {
chunks: 'all'
}
},
plugins: [ /* Various Plugin */ ],
module: {
rules: [ /* My Rules */ ]
}
}
Any one know why my HOC is undefined?
Edit:
I've placed Console Log in the tree file. The result are:
'Login Loaded' - undefined
'withAuth Loaded'
'NavBar Loaded' - function() { }
Edit 2:
This is the files structure:
app/
|-high-order-component/
| |-auth/
| |-withAuth.js
|
|-layout-component/
| |-navbar/
| |-index.js
|
|-pages/
|-auth/
|-login.js
Resolved
After much testing and research throughout the afternoon I came to the solution of the problem. As I said in the question, mine is a larger project and I only partially wrote its structure because I thought the problem was located in those three files.
In reality, the problem was a Circular Dependency problem and not a Webpack configuration problem.
In my project I have a module called 'Route' that store all Path and all Component for Path, so I can build the React Router using Array Map function. That module has a function that allow me to Route through path and that can return me a path string to a Component.
My problem was due to the fact that this module is often called in the project and this has created a Circular Dependency.
Webpack doesn't show the Circular Dependency during compiling, but I found useful adding a plugin, called CircualDependencyPlugin. This plugin will break Webpack compiling when a Circual Dependency will be found.
Splitting the Route module into two files solved my problem.

How to use Webpack to load a static file with a relative path with React?

I'm trying to create a map component in React using the Tangram Library.
I got it working with Webpack alone but it started bugging out when I used react in the mix.
I've tried using various loaders such as a raw loader, a yaml loader and so forth, but none of them have worked thus far.
The map component looks as follows:
// -- Import dependencies --
import React from 'react';
import { Map } from 'react-leaflet';
// -- Import Tangram --
import Tangram from 'tangram';
import yaml from 'js-yaml';
import data from '!!raw-loader!./scene.yaml';
export default class LeafletMap extends React.Component {
componentDidMount() {
const layer = Tangram.leafletLayer({
scene: data
});
layer.addTo(this.map.leafletElement);
}
render() {
return (
<Map center={[40.70532, -74.00976]} zoom={15} ref={(ref) => { this.map = ref }} />
);
}
}
How can I actually load the scene.yaml so that the Tangram library makes use of it ?
In the end it responds with a 404 as the file isn't found.
The solution was, that the static files weren't being copied to the bundle built by webpack.
I solved it by using the CopyPlugin in the webpack config and copying the files to a folder respecting the relative path name, like so:
const CopyPlugin = require('copy-webpack-plugin');
module.exports = {
entry: './main.js',
output: {
filename: './bundle.js'
},
plugins: [
new CopyPlugin([
{ from: 'src', to: 'src' },
]),
],
};

Include pictures in JS bundle

My case is the following :
I create a components library for React. So I have a package (bundled with Rollup) that include some pictures (For now only a GIF picture that is used in a component).
The component that use my picture is like this :
import React from 'react';
import PropTypes from 'prop-types';
import ui_spinner from '../../../assets/ui_progress.gif';
/**
* CircularSpinner
* Add a spinner when the user needs to wait
*/
class CircularSpinner extends React.PureComponent {
static propTypes = {
/** Width of the component */
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/** Height of the component */
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
/** Style of the component (overload existing properties) */
style: PropTypes.object,
}
static defaultProps = {
width: 128,
height: 128,
style: {},
}
render() {
const { style, width, height } = this.props;
return (
<img src={ui_spinner} width={width} height={height} style={style} alt="ui_progress" aria-busy="true" />
);
}
}
export default CircularSpinner;
When I built it, it's OK.
Now I create a React application with create-react-app and I want to test my components library. To do this, I use npm link (To avoid to push deploy my npm package). My components are OK in my testing application but the picture (the GIF in my CircularSpinner component) is not displayed.
So my question is the following : How to include some assets in a JS bundle with Rollup ? My working approach is correct ?
My Rollup config is the following :
import { uglify } from 'rollup-plugin-uglify'
import babel from 'rollup-plugin-babel'
import url from 'rollup-plugin-url'
const config = {
input: 'src/index.js',
external: ['react'],
output: {
format: 'umd',
name: 'react-components',
globals: {
react: "React"
}
},
plugins: [
babel({
exclude: "node_modules/**"
}),
uglify(),
url(),
]
}
export default config
I build with rollup -c -o dist/index.js.
And the dist folder has the following content :
dist/
assets
92be5c546b4adf43.gif
index.js
My component that use my picture is like this in my testing application :
<img src="92be5c546b4adf43.gif" width="128" height="128" alt="ui_progress" aria-busy="true">
Thanks for your help !
Damien
I found the solution for this issue. This response may help someone :
I update my rollup config to use rollup-plugin-img. I have already used it but my configuration was not correct :
The correct config is the following :
import { uglify } from 'rollup-plugin-uglify'
import babel from 'rollup-plugin-babel'
import image from 'rollup-plugin-img'
const config = {
input: 'src/index.js',
external: ['react'],
output: {
format: 'umd',
name: 'react-components',
globals: {
react: "React"
}
},
plugins: [
babel({
exclude: "node_modules/**"
}),
image({
limit: 100000,
}),
uglify(),
]
}
export default config
My error was that my GIF is a little big and the default limit size is 8192 bytes.
In this case, I have the following error :
Error: Could not load <path of image> (imported by <path of component that use image>): The "path" argument must be of type string. Received type undefined
When I have updated my config to increase the limit, everything is OK

How to use external html templates in Components with Vue.js 2.0 in Laravel 5.3

I am using the default Laravel 5.3 setup - a fresh install with default configuration for Vue.js 2.0.
With Laravel 5.1 and Vue.js 1.x, I could easily define components like and used browserify to compile.
Vue.component('test-component', require('./test-component');
/* test-component.js */
export default{
template:require('./test-component.template.html')
}
/* test-component.template.html */
<div class="test-component">
<h1> Test Component <h1>
</div>
However, with Vue 2, the default is webpack (although browserify is also available but could not get it working and webpack seems better). But I am not able to get the configuration working.
I have tried various configurations, finally I have this in my gulp.js file
const elixir = require('laravel-elixir');
require('laravel-elixir-vue-2');
var config = {
module:{
loaders:[
{
test: /\.html$/,
loader: 'vue-loader'
}
]
}
};
elixir(mix => {
mix.sass('app.scss')
.webpack('app.js',null, null, config);
});
Now I am not getting any errors while compiling gulp webpack however when I try to view the page in the browser, it doesn't show the component and has an error/warn in the console
vue.js?3de6:513[Vue warn]: invalid template option:[object Object]
(found in component <test>)
vue.js?3de6:4085Uncaught TypeError: Cannot read property 'child' of null(…)
My app.js main entry file (default provided by Laravel)
require('./bootstrap');
Vue.component('test-component', require('./components/test-component'));
const app = new Vue({
//el: '#app',
}).$mount('#app');
What am I missing? Any pointers would be appreciated.
You have to use html-loader instead of vue-loader.
npm install html-loader --save-dev
Your gulpfile.js (need to change the loader):
const config = {
module: {
loaders:[{
test: /\.html$/,
loader: 'html'
}]
}
};
elixir((mix) => {
mix.sass('app.scss')
.webpack('app.js', null, null, config);
});
Your test-component.js (no changes, you're good to go):
export default {
template: require('./test-component.template.html')
}
Your test-component-template.html: (no changes, you're good to go)
<div class="test-component">
<h1>Test Component Goes Here</h1>
</div>
Important
Here's how to define your component:
Using Default
Vue.component('test-component', require('./test-component').default);
Or, simply use ES2015 import
import TestComponent from './test-component';
Vue.component('test-component', TestComponent);
Try this in your gulp file:
const elixir = require('laravel-elixir');
require('laravel-elixir-vue-2');
elixir(mix => {
mix.webpack('app.js');
});
And import your components like this:
import Test from './Components/Test.vue';
Vue.component('test', Test);

Categories

Resources