mirror of
https://github.com/oceanprotocol/ipfs
synced 2024-11-21 17:26:59 +01:00
initial commit 🏄♀️
This commit is contained in:
commit
f356f3ba29
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
dist
|
||||
package-lock.json
|
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2
|
||||
}
|
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# IPFS
|
||||
|
||||
> 🏄♀️ Ocean Protocol's public IPFS Node, setup to be a public gateway, and to provide some access to its HTTP API for everyone.
|
||||
> [ipfs.oceanprotocol.com](https://ipfs.oceanprotocol.com)
|
24
config.js
Normal file
24
config.js
Normal file
@ -0,0 +1,24 @@
|
||||
export const title = 'Ocean Protocol 💖 IPFS'
|
||||
export const description = `Ocean Protocol's public IPFS Node, setup to be a public gateway, and to provide some access to its HTTP API for everyone.<br /><a href="https://blog.oceanprotocol.com/ocean-and-ipfs-sitting-in-the-merkle-tree-43c623c356d7">Learn More →</a>`
|
||||
|
||||
export const ipfsGateway = 'https://ipfs.oceanprotocol.com'
|
||||
export const ipfsNodeUri = 'https://ipfs.oceanprotocol.com:443'
|
||||
|
||||
export const links = [
|
||||
{
|
||||
name: 'Homepage',
|
||||
url: 'https://oceanprotocol.com'
|
||||
},
|
||||
{
|
||||
name: 'Blog',
|
||||
url: 'https://blog.oceanprotocol.com'
|
||||
},
|
||||
{
|
||||
name: 'Twitter',
|
||||
url: 'https://twitter.com/oceanprotocol'
|
||||
},
|
||||
{
|
||||
name: 'GitHub',
|
||||
url: 'https://github.com/oceanprotocol'
|
||||
}
|
||||
]
|
53
package.json
Normal file
53
package.json
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "@oceanprotocol/ipfs",
|
||||
"version": "1.0.0",
|
||||
"description": "Ocean Protocol's Public IPFS Node.",
|
||||
"scripts": {
|
||||
"start": "webpack-dev-server",
|
||||
"build": "NODE_ENV=production webpack"
|
||||
},
|
||||
"author": "Matthias Kretschmann <matthias@oceanprotocol.com>",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@oceanprotocol/art": "^2.2.0",
|
||||
"@oceanprotocol/typographies": "^0.1.0",
|
||||
"ipfs-http-client": "^38.2.0",
|
||||
"react-dropzone": "^10.1.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.6.4",
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@babel/polyfill": "^7.6.0",
|
||||
"@babel/preset-env": "^7.6.3",
|
||||
"@babel/preset-react": "^7.6.3",
|
||||
"@svgr/webpack": "^4.3.3",
|
||||
"babel-loader": "^8.0.6",
|
||||
"copy-webpack-plugin": "^5.0.4",
|
||||
"css-loader": "^3.2.0",
|
||||
"eslint": "^6.5.1",
|
||||
"eslint-config-oceanprotocol": "^1.5.0",
|
||||
"eslint-config-prettier": "^6.4.0",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-react": "^7.16.0",
|
||||
"mini-css-extract-plugin": "^0.8.0",
|
||||
"prettier": "^1.18.2",
|
||||
"react": "^16.10.2",
|
||||
"react-dom": "^16.10.2",
|
||||
"style-loader": "^1.0.0",
|
||||
"webpack": "^4.41.2",
|
||||
"webpack-cli": "^3.3.9",
|
||||
"webpack-dev-server": "^3.8.2"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"oceanprotocol",
|
||||
"oceanprotocol/react",
|
||||
"prettier/react",
|
||||
"prettier/standard",
|
||||
"plugin:prettier/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier"
|
||||
]
|
||||
}
|
||||
}
|
46
src/App.css
Normal file
46
src/App.css
Normal file
@ -0,0 +1,46 @@
|
||||
@import 'styles/_variables.css';
|
||||
|
||||
.app {
|
||||
padding: var(--spacer);
|
||||
background: var(--brand-black);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.app {
|
||||
height: auto;
|
||||
min-height: calc(100vh - (var(--page-frame) * 2) - (var(--spacer) * 2));
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
margin-top: var(--spacer);
|
||||
}
|
||||
|
||||
.appTitle {
|
||||
margin-top: var(--spacer);
|
||||
font-size: var(--font-size-h2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
.appTitle {
|
||||
font-size: var(--font-size-h1);
|
||||
}
|
||||
}
|
||||
|
||||
.appDescription {
|
||||
font-size: var(--font-size-large);
|
||||
color: var(--brand-grey-lighter);
|
||||
max-width: 40rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
25
src/App.jsx
Normal file
25
src/App.jsx
Normal file
@ -0,0 +1,25 @@
|
||||
import '@babel/polyfill'
|
||||
import React from 'react'
|
||||
import Add from './components/Add'
|
||||
import Logo from '@oceanprotocol/art/logo/logo-white.svg'
|
||||
import './styles/global.css'
|
||||
import './App.css'
|
||||
import { title, description } from '../config'
|
||||
import Footer from './components/Footer'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="app">
|
||||
<header className="header">
|
||||
<Logo className="logo" />
|
||||
<h1 className="appTitle">{title}</h1>
|
||||
<p
|
||||
className="appDescription"
|
||||
dangerouslySetInnerHTML={{ __html: description }}
|
||||
/>
|
||||
</header>
|
||||
<Add />
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
4
src/components/Add.css
Normal file
4
src/components/Add.css
Normal file
@ -0,0 +1,4 @@
|
||||
.add {
|
||||
max-width: 40rem;
|
||||
width: 100%;
|
||||
}
|
38
src/components/Add.jsx
Normal file
38
src/components/Add.jsx
Normal file
@ -0,0 +1,38 @@
|
||||
import React, { useState } from 'react'
|
||||
import { saveToIpfs } from '../ipfs'
|
||||
import { ipfsGateway } from '../../config'
|
||||
import Dropzone from './Dropzone'
|
||||
import './Add.css'
|
||||
import Spinner from './Spinner'
|
||||
|
||||
export default function Add() {
|
||||
const [fileHash, setFileHash] = useState(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const handleCaptureFile = async files => {
|
||||
setLoading(true)
|
||||
const cid = await saveToIpfs(files)
|
||||
setFileHash(cid)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="add">
|
||||
{loading ? (
|
||||
<Spinner />
|
||||
) : fileHash ? (
|
||||
<div>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`${ipfsGateway}/ipfs/${fileHash}`}
|
||||
>
|
||||
{fileHash}
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<Dropzone multiple={false} handleOnDrop={handleCaptureFile} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
31
src/components/Dropzone.css
Normal file
31
src/components/Dropzone.css
Normal file
@ -0,0 +1,31 @@
|
||||
@import '../styles/_variables.css';
|
||||
|
||||
.dropzone {
|
||||
border: 0.2rem dashed var(--brand-grey-light);
|
||||
border-radius: calc(var(--border-radius) * 2);
|
||||
padding: calc(var(--spacer) * 2);
|
||||
transition: 0.2s ease-out;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dropzone:hover,
|
||||
.dropzone:focus,
|
||||
.dropzone:active {
|
||||
border-color: var(--brand-grey-lighter);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.dropzone.dragover {
|
||||
border-color: var(--brand-pink);
|
||||
}
|
||||
|
||||
.dropzone.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.dropzone p {
|
||||
text-align: center;
|
||||
margin-bottom: 0;
|
||||
color: var(--brand-grey-light);
|
||||
}
|
45
src/components/Dropzone.jsx
Normal file
45
src/components/Dropzone.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
import { useDropzone } from 'react-dropzone'
|
||||
import './Dropzone.css'
|
||||
|
||||
Dropzone.propTypes = {
|
||||
handleOnDrop: PropTypes.func.isRequired,
|
||||
disabled: PropTypes.bool,
|
||||
multiple: PropTypes.bool
|
||||
}
|
||||
|
||||
export default function Dropzone({ handleOnDrop, disabled, multiple }) {
|
||||
const onDrop = useCallback(acceptedFiles => handleOnDrop(acceptedFiles), [
|
||||
handleOnDrop
|
||||
])
|
||||
|
||||
const {
|
||||
getRootProps,
|
||||
getInputProps,
|
||||
isDragActive,
|
||||
isDragReject
|
||||
} = useDropzone({ onDrop })
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getRootProps({
|
||||
className: isDragActive
|
||||
? 'dragover'
|
||||
: disabled
|
||||
? 'disabled'
|
||||
: 'dropzone'
|
||||
})}
|
||||
>
|
||||
<input {...getInputProps({ multiple })} />
|
||||
<p>
|
||||
{isDragActive && !isDragReject
|
||||
? `Drop it like it's hot!`
|
||||
: multiple
|
||||
? `Drag 'n' drop some files here, or click to select files`
|
||||
: `Drag 'n' drop a file here, or click to select a file`}
|
||||
{}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
31
src/components/Footer.css
Normal file
31
src/components/Footer.css
Normal file
@ -0,0 +1,31 @@
|
||||
@import '../styles/_variables.css';
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
font-size: var(--font-size-mini);
|
||||
padding: var(--spacer) 0;
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-self: flex-end;
|
||||
text-align: left;
|
||||
margin-top: var(--spacer);
|
||||
}
|
||||
|
||||
.footer,
|
||||
.footer a {
|
||||
color: var(--brand-grey-light);
|
||||
}
|
||||
|
||||
.footer > div {
|
||||
flex: 1 1 48%;
|
||||
}
|
||||
|
||||
.footer > div:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.footer > div:last-child a {
|
||||
display: inline-block;
|
||||
margin-left: var(--spacer);
|
||||
}
|
22
src/components/Footer.jsx
Normal file
22
src/components/Footer.jsx
Normal file
@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import { links } from '../../config'
|
||||
import './Footer.css'
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div>
|
||||
© <span id="year">{Date.now()}</span>{' '}
|
||||
<a href="https://oceanprotocol.com">Ocean Protocol Foundation Ltd.</a> —
|
||||
All Rights Reserved
|
||||
</div>
|
||||
<div>
|
||||
{links.map(link => (
|
||||
<a key={link.name} href={link.url}>
|
||||
{link.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
36
src/components/Spinner.css
Normal file
36
src/components/Spinner.css
Normal file
@ -0,0 +1,36 @@
|
||||
@import '../styles/_variables.css';
|
||||
|
||||
.spinner {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
margin-top: calc(var(--spacer) * var(--line-height));
|
||||
margin-bottom: calc(var(--spacer) / 2);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.spinner:before {
|
||||
content: '';
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: -20px;
|
||||
margin-left: -10px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--brand-purple);
|
||||
border-top-color: var(--brand-violet);
|
||||
animation: spinner 0.6s linear infinite;
|
||||
}
|
||||
|
||||
.spinnerMessage {
|
||||
color: var(--brand-grey-light);
|
||||
padding-top: calc(var(--spacer) / 4);
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
6
src/components/Spinner.jsx
Normal file
6
src/components/Spinner.jsx
Normal file
@ -0,0 +1,6 @@
|
||||
import React from 'react'
|
||||
import './Spinner.css'
|
||||
|
||||
export default function Spinner() {
|
||||
return <div className="spinner" />
|
||||
}
|
13
src/index.html
Normal file
13
src/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<title>Ocean Protocol 💖 IPFS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
5
src/index.js
Normal file
5
src/index.js
Normal file
@ -0,0 +1,5 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
|
||||
ReactDOM.render(<App />, document.getElementById('root'))
|
35
src/ipfs.js
Normal file
35
src/ipfs.js
Normal file
@ -0,0 +1,35 @@
|
||||
import ipfsClient from 'ipfs-http-client'
|
||||
import { ipfsNodeUri } from '../config'
|
||||
|
||||
export async function saveToIpfs(files) {
|
||||
const { hostname, port, protocol } = new URL(ipfsNodeUri)
|
||||
|
||||
const ipfsConfig = {
|
||||
protocol: protocol.replace(':', ''),
|
||||
host: hostname,
|
||||
port: port || '443'
|
||||
}
|
||||
|
||||
const ipfs = ipfsClient(ipfsConfig)
|
||||
|
||||
const file = [...files][0]
|
||||
let ipfsId
|
||||
const fileDetails = {
|
||||
path: file.name,
|
||||
content: file
|
||||
}
|
||||
const options = {
|
||||
wrapWithDirectory: true,
|
||||
progress: prog => console.log(`received: ${prog}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await ipfs.add(fileDetails, options)
|
||||
|
||||
// CID of wrapping directory is returned last
|
||||
ipfsId = `${response[response.length - 1].hash}/${fileDetails.path}`
|
||||
return ipfsId
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
}
|
||||
}
|
51
src/styles/_variables.css
Normal file
51
src/styles/_variables.css
Normal file
@ -0,0 +1,51 @@
|
||||
:root {
|
||||
--brand-white: #fff;
|
||||
--brand-black: #141414;
|
||||
--brand-pink: #ff4092;
|
||||
--brand-purple: #7b1173;
|
||||
--brand-violet: #e000cf;
|
||||
--brand-blue: #11597b;
|
||||
|
||||
--brand-grey: #41474e;
|
||||
--brand-grey-light: #8b98a9;
|
||||
--brand-grey-dark: #303030;
|
||||
--brand-grey-lighter: #e2e2e2;
|
||||
|
||||
--green: #5fb359;
|
||||
--red: #d80606;
|
||||
--orange: #b35f36;
|
||||
--yellow: #eac146;
|
||||
|
||||
--font-family-base: 'Sharp Sans', -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
--font-family-title: 'Sharp Sans Display', -apple-system, BlinkMacSystemFont,
|
||||
'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
--font-family-monospace: 'Fira Code', 'Fira Mono', Menlo, Monaco, Consolas,
|
||||
'Courier New', monospace;
|
||||
|
||||
--font-size-root: 16px;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-large: 1.2rem;
|
||||
--font-size-small: 0.85rem;
|
||||
--font-size-mini: 0.65rem;
|
||||
--font-size-text: $font-size-base;
|
||||
--font-size-label: $font-size-base;
|
||||
--font-size-title: 1.4rem;
|
||||
--font-size-h1: 3rem;
|
||||
--font-size-h2: 1.7rem;
|
||||
--font-size-h3: 1.4rem;
|
||||
--font-size-h4: $font-size-large;
|
||||
--font-size-h5: 1.1rem;
|
||||
--font-weight-base: 500;
|
||||
--font-weight-bold: 600;
|
||||
--line-height: 1.6;
|
||||
|
||||
--spacer: 2rem;
|
||||
--page-frame: 0.75rem;
|
||||
|
||||
--break-point--small: 640px;
|
||||
--break-point--medium: 860px;
|
||||
--break-point--large: 1140px;
|
||||
--break-point--huge: 1400px;
|
||||
--border-radius: 0.2rem;
|
||||
}
|
90
src/styles/global.css
Normal file
90
src/styles/global.css
Normal file
@ -0,0 +1,90 @@
|
||||
@import '../../node_modules/@oceanprotocol/typographies/css/ocean-typo.css';
|
||||
@import '_variables.css';
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
max-height: calc(100vh - (var(--page-frame) * 2));
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
font-size: var(--font-size-root);
|
||||
}
|
||||
|
||||
body {
|
||||
color: var(--brand-grey-light);
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family-base);
|
||||
font-weight: var(--font-weight-base);
|
||||
line-height: var(--line-height);
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 640px) {
|
||||
body {
|
||||
padding: var(--page-frame);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--brand-pink);
|
||||
transition: 0.2s ease-out;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
margin-bottom: calc(var(--spacer) / var(--line-height));
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
font-family: var(--font-family-title);
|
||||
color: var(--brand-white);
|
||||
line-height: 1.2;
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-h1);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-h2);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-h3);
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: var(--font-size-h4);
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: var(--font-size-h5);
|
||||
}
|
||||
|
||||
figure,
|
||||
img,
|
||||
svg,
|
||||
video,
|
||||
audio,
|
||||
embed,
|
||||
canvas,
|
||||
picture {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
68
webpack.config.js
Normal file
68
webpack.config.js
Normal file
@ -0,0 +1,68 @@
|
||||
const path = require('path')
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
|
||||
const CopyPlugin = require('copy-webpack-plugin')
|
||||
|
||||
const defaultInclude = [path.resolve(__dirname, 'src')]
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
|
||||
module.exports = {
|
||||
mode: isDevelopment ? 'development' : 'production',
|
||||
entry: path.resolve(__dirname, 'src', 'index.js'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
include: defaultInclude,
|
||||
use: [
|
||||
{
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env', '@babel/preset-react'],
|
||||
plugins: ['@babel/plugin-proposal-class-properties']
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
include: defaultInclude
|
||||
},
|
||||
{
|
||||
test: /\.(jpe?g|png|gif)$/,
|
||||
use: ['file-loader?name=img/[name]__[hash:base64:5].[ext]'],
|
||||
include: defaultInclude
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{
|
||||
loader: '@svgr/webpack',
|
||||
options: {
|
||||
icon: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['*', '.js', '.jsx']
|
||||
},
|
||||
node: {
|
||||
fs: 'empty',
|
||||
net: 'empty',
|
||||
tls: 'empty'
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({
|
||||
filename: isDevelopment ? '[name].css' : '[name].[hash].css',
|
||||
chunkFilename: isDevelopment ? '[id].css' : '[id].[hash].css'
|
||||
}),
|
||||
new CopyPlugin([{ from: './src/index.html', to: './', flatten: true }])
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user