mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add JavaScript direct upload support (#81)
This commit is contained in:
parent
dc235c4d13
commit
6262891b56
16 changed files with 3139 additions and 0 deletions
5
.babelrc
Normal file
5
.babelrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
["env", { "modules": false } ]
|
||||||
|
]
|
||||||
|
}
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
.byebug_history
|
.byebug_history
|
||||||
|
/node_modules
|
||||||
test/dummy/db/*.sqlite3
|
test/dummy/db/*.sqlite3
|
||||||
test/dummy/db/*.sqlite3-journal
|
test/dummy/db/*.sqlite3-journal
|
||||||
test/dummy/log/*.log
|
test/dummy/log/*.log
|
||||||
|
|
38
README.md
38
README.md
|
@ -96,6 +96,44 @@ Variation of image attachment:
|
||||||
6. Optional: Add `gem "google-cloud-storage", "~> 1.3"` to your Gemfile if you want to use Google Cloud Storage.
|
6. Optional: Add `gem "google-cloud-storage", "~> 1.3"` to your Gemfile if you want to use Google Cloud Storage.
|
||||||
7. Optional: Add `gem "mini_magick"` to your Gemfile if you want to use variants.
|
7. Optional: Add `gem "mini_magick"` to your Gemfile if you want to use variants.
|
||||||
|
|
||||||
|
## Direct uploads
|
||||||
|
|
||||||
|
Active Storage, with its included JavaScript library, supports uploading directly from the client to the cloud.
|
||||||
|
|
||||||
|
### Direct upload installation
|
||||||
|
|
||||||
|
1. Include `activestorage.js` in your application's JavaScript bundle.
|
||||||
|
|
||||||
|
Using the asset pipeline:
|
||||||
|
```js
|
||||||
|
//= require activestorage
|
||||||
|
```
|
||||||
|
Using the npm package:
|
||||||
|
```js
|
||||||
|
import * as ActiveStorage from "activestorage"
|
||||||
|
ActiveStorage.start()
|
||||||
|
```
|
||||||
|
2. Annotate file inputs with the direct upload URL.
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
<%= form.file_field :attachments, multiple: true, data: { direct_upload_url: rails_direct_uploads_url } %>
|
||||||
|
```
|
||||||
|
3. That's it! Uploads begin upon form submission.
|
||||||
|
|
||||||
|
### Direct upload JavaScript events
|
||||||
|
|
||||||
|
| Event name | Event target | Event data (`event.detail`) | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `direct-uploads:start` | `<form>` | None | A form containing files for direct upload fields was submit. |
|
||||||
|
| `direct-upload:initialize` | `<input>` | `{id, file}` | Dispatched for every file after form submission. |
|
||||||
|
| `direct-upload:start` | `<input>` | `{id, file}` | A direct upload is starting. |
|
||||||
|
| `direct-upload:before-blob-request` | `<input>` | `{id, file, xhr}` | Before making a request to your application for direct upload metadata. |
|
||||||
|
| `direct-upload:before-storage-request` | `<input>` | `{id, file, xhr}` | Before making a request to store a file. |
|
||||||
|
| `direct-upload:progress` | `<input>` | `{id, file, progress}` | As requests to store files progress. |
|
||||||
|
| `direct-upload:error` | `<input>` | `{id, file, error}` | An error occurred. An `alert` will display unless this event is canceled. |
|
||||||
|
| `direct-upload:end` | `<input>` | `{id, file}` | A direct upload has ended. |
|
||||||
|
| `direct-uploads:end` | `<form>` | None | All direct uploads have ended. |
|
||||||
|
|
||||||
## Compatibility & Expectations
|
## Compatibility & Expectations
|
||||||
|
|
||||||
Active Storage only works with the development version of Rails 5.2+ (as of July 19, 2017). This separate repository is a staging ground for the upcoming inclusion in rails/rails prior to the Rails 5.2 release. It is not intended to be a long-term stand-alone repository.
|
Active Storage only works with the development version of Rails 5.2+ (as of July 19, 2017). This separate repository is a staging ground for the upcoming inclusion in rails/rails prior to the Rails 5.2 release. It is not intended to be a long-term stand-alone repository.
|
||||||
|
|
1
app/assets/javascripts/activestorage.js
Normal file
1
app/assets/javascripts/activestorage.js
Normal file
File diff suppressed because one or more lines are too long
52
app/javascript/activestorage/blob_record.js
Normal file
52
app/javascript/activestorage/blob_record.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { getMetaValue } from "./helpers"
|
||||||
|
|
||||||
|
export class BlobRecord {
|
||||||
|
constructor(file, checksum, url) {
|
||||||
|
this.file = file
|
||||||
|
|
||||||
|
this.attributes = {
|
||||||
|
filename: file.name,
|
||||||
|
content_type: file.type,
|
||||||
|
byte_size: file.size,
|
||||||
|
checksum: checksum
|
||||||
|
}
|
||||||
|
|
||||||
|
this.xhr = new XMLHttpRequest
|
||||||
|
this.xhr.open("POST", url, true)
|
||||||
|
this.xhr.responseType = "json"
|
||||||
|
this.xhr.setRequestHeader("Content-Type", "application/json")
|
||||||
|
this.xhr.setRequestHeader("Accept", "application/json")
|
||||||
|
this.xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
|
||||||
|
this.xhr.setRequestHeader("X-CSRF-Token", getMetaValue("csrf-token"))
|
||||||
|
this.xhr.addEventListener("load", event => this.requestDidLoad(event))
|
||||||
|
this.xhr.addEventListener("error", event => this.requestDidError(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
create(callback) {
|
||||||
|
this.callback = callback
|
||||||
|
this.xhr.send(JSON.stringify({ blob: this.attributes }))
|
||||||
|
}
|
||||||
|
|
||||||
|
requestDidLoad(event) {
|
||||||
|
const { status, response } = this.xhr
|
||||||
|
if (status >= 200 && status < 300) {
|
||||||
|
this.attributes.signed_id = response.signed_blob_id
|
||||||
|
this.uploadURL = response.upload_to_url
|
||||||
|
this.callback(null, this.toJSON())
|
||||||
|
} else {
|
||||||
|
this.requestDidError(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestDidError(event) {
|
||||||
|
this.callback(`Error creating Blob for "${this.file.name}". Status: ${this.xhr.status}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON() {
|
||||||
|
const result = {}
|
||||||
|
for (const key in this.attributes) {
|
||||||
|
result[key] = this.attributes[key]
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
31
app/javascript/activestorage/blob_upload.js
Normal file
31
app/javascript/activestorage/blob_upload.js
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
export class BlobUpload {
|
||||||
|
constructor(blob) {
|
||||||
|
this.blob = blob
|
||||||
|
this.file = blob.file
|
||||||
|
|
||||||
|
this.xhr = new XMLHttpRequest
|
||||||
|
this.xhr.open("PUT", blob.uploadURL, true)
|
||||||
|
this.xhr.setRequestHeader("Content-Type", blob.attributes.content_type)
|
||||||
|
this.xhr.setRequestHeader("Content-MD5", blob.attributes.checksum)
|
||||||
|
this.xhr.addEventListener("load", event => this.requestDidLoad(event))
|
||||||
|
this.xhr.addEventListener("error", event => this.requestDidError(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
create(callback) {
|
||||||
|
this.callback = callback
|
||||||
|
this.xhr.send(this.file)
|
||||||
|
}
|
||||||
|
|
||||||
|
requestDidLoad(event) {
|
||||||
|
const { status, response } = this.xhr
|
||||||
|
if (status >= 200 && status < 300) {
|
||||||
|
this.callback(null, this.xhr.response)
|
||||||
|
} else {
|
||||||
|
this.requestDidError(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestDidError(event) {
|
||||||
|
this.callback(`Error storing "${this.file.name}". Status: ${this.xhr.status}`)
|
||||||
|
}
|
||||||
|
}
|
43
app/javascript/activestorage/direct_upload.js
Normal file
43
app/javascript/activestorage/direct_upload.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import { FileChecksum } from "./file_checksum"
|
||||||
|
import { BlobRecord } from "./blob_record"
|
||||||
|
import { BlobUpload } from "./blob_upload"
|
||||||
|
|
||||||
|
let id = 0
|
||||||
|
|
||||||
|
export class DirectUpload {
|
||||||
|
constructor(file, options = {}) {
|
||||||
|
this.id = id++
|
||||||
|
this.file = file
|
||||||
|
this.url = options.url
|
||||||
|
this.delegate = options.delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
create(callback) {
|
||||||
|
const fileChecksum = new FileChecksum(this.file)
|
||||||
|
fileChecksum.create((error, checksum) => {
|
||||||
|
const blob = new BlobRecord(this.file, checksum, this.url)
|
||||||
|
notify(this.delegate, "directUploadWillCreateBlobWithXHR", blob.xhr)
|
||||||
|
blob.create(error => {
|
||||||
|
if (error) {
|
||||||
|
callback(error)
|
||||||
|
} else {
|
||||||
|
const upload = new BlobUpload(blob)
|
||||||
|
notify(this.delegate, "directUploadWillStoreFileWithXHR", upload.xhr)
|
||||||
|
upload.create(error => {
|
||||||
|
if (error) {
|
||||||
|
callback(error)
|
||||||
|
} else {
|
||||||
|
callback(null, blob.toJSON())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify(object, methodName, ...messages) {
|
||||||
|
if (object && typeof object[methodName] == "function") {
|
||||||
|
return object[methodName](...messages)
|
||||||
|
}
|
||||||
|
}
|
67
app/javascript/activestorage/direct_upload_controller.js
Normal file
67
app/javascript/activestorage/direct_upload_controller.js
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
import { DirectUpload } from "./direct_upload"
|
||||||
|
import { dispatchEvent } from "./helpers"
|
||||||
|
|
||||||
|
export class DirectUploadController {
|
||||||
|
constructor(input, file) {
|
||||||
|
this.input = input
|
||||||
|
this.file = file
|
||||||
|
this.directUpload = new DirectUpload(this.file, { url: this.url, delegate: this })
|
||||||
|
this.dispatch("initialize")
|
||||||
|
}
|
||||||
|
|
||||||
|
start(callback) {
|
||||||
|
const hiddenInput = document.createElement("input")
|
||||||
|
hiddenInput.type = "hidden"
|
||||||
|
hiddenInput.name = this.input.name
|
||||||
|
this.input.insertAdjacentElement("beforebegin", hiddenInput)
|
||||||
|
|
||||||
|
this.dispatch("start")
|
||||||
|
|
||||||
|
this.directUpload.create((error, attributes) => {
|
||||||
|
if (error) {
|
||||||
|
hiddenInput.parentNode.removeChild(hiddenInput)
|
||||||
|
this.dispatchError(error)
|
||||||
|
} else {
|
||||||
|
hiddenInput.value = attributes.signed_id
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatch("end")
|
||||||
|
callback(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadRequestDidProgress(event) {
|
||||||
|
const progress = event.loaded / event.total * 100
|
||||||
|
if (progress) {
|
||||||
|
this.dispatch("progress", { progress })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
return this.input.getAttribute("data-direct-upload-url")
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(name, detail = {}) {
|
||||||
|
detail.file = this.file
|
||||||
|
detail.id = this.directUpload.id
|
||||||
|
return dispatchEvent(this.input, `direct-upload:${name}`, { detail })
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchError(error) {
|
||||||
|
const event = this.dispatch("error", { error })
|
||||||
|
if (!event.defaultPrevented) {
|
||||||
|
alert(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirectUpload delegate
|
||||||
|
|
||||||
|
directUploadWillCreateBlobWithXHR(xhr) {
|
||||||
|
this.dispatch("before-blob-request", { xhr })
|
||||||
|
}
|
||||||
|
|
||||||
|
directUploadWillStoreFileWithXHR(xhr) {
|
||||||
|
this.dispatch("before-storage-request", { xhr })
|
||||||
|
xhr.upload.addEventListener("progress", event => this.uploadRequestDidProgress(event))
|
||||||
|
}
|
||||||
|
}
|
50
app/javascript/activestorage/direct_uploads_controller.js
Normal file
50
app/javascript/activestorage/direct_uploads_controller.js
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { DirectUploadController } from "./direct_upload_controller"
|
||||||
|
import { findElements, dispatchEvent, toArray } from "./helpers"
|
||||||
|
|
||||||
|
const inputSelector = "input[type=file][data-direct-upload-url]:not([disabled])"
|
||||||
|
|
||||||
|
export class DirectUploadsController {
|
||||||
|
constructor(form) {
|
||||||
|
this.form = form
|
||||||
|
this.inputs = findElements(form, inputSelector).filter(input => input.files.length)
|
||||||
|
}
|
||||||
|
|
||||||
|
start(callback) {
|
||||||
|
const controllers = this.createDirectUploadControllers()
|
||||||
|
|
||||||
|
const startNextController = () => {
|
||||||
|
const controller = controllers.shift()
|
||||||
|
if (controller) {
|
||||||
|
controller.start(error => {
|
||||||
|
if (error) {
|
||||||
|
callback(error)
|
||||||
|
this.dispatch("end")
|
||||||
|
} else {
|
||||||
|
startNextController()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
callback()
|
||||||
|
this.dispatch("end")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatch("start")
|
||||||
|
startNextController()
|
||||||
|
}
|
||||||
|
|
||||||
|
createDirectUploadControllers() {
|
||||||
|
const controllers = []
|
||||||
|
this.inputs.forEach(input => {
|
||||||
|
toArray(input.files).forEach(file => {
|
||||||
|
const controller = new DirectUploadController(input, file)
|
||||||
|
controllers.push(controller)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return controllers
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(name, detail = {}) {
|
||||||
|
return dispatchEvent(this.form, `direct-uploads:${name}`, { detail })
|
||||||
|
}
|
||||||
|
}
|
48
app/javascript/activestorage/file_checksum.js
Normal file
48
app/javascript/activestorage/file_checksum.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import SparkMD5 from "spark-md5"
|
||||||
|
|
||||||
|
const fileSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
|
||||||
|
|
||||||
|
export class FileChecksum {
|
||||||
|
constructor(file) {
|
||||||
|
this.file = file
|
||||||
|
this.chunkSize = 2097152 // 2MB
|
||||||
|
this.chunkCount = Math.ceil(this.file.size / this.chunkSize)
|
||||||
|
this.chunkIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
create(callback) {
|
||||||
|
this.callback = callback
|
||||||
|
this.md5Buffer = new SparkMD5.ArrayBuffer
|
||||||
|
this.fileReader = new FileReader
|
||||||
|
this.fileReader.addEventListener("load", event => this.fileReaderDidLoad(event))
|
||||||
|
this.fileReader.addEventListener("error", event => this.fileReaderDidError(event))
|
||||||
|
this.readNextChunk()
|
||||||
|
}
|
||||||
|
|
||||||
|
fileReaderDidLoad(event) {
|
||||||
|
this.md5Buffer.append(event.target.result)
|
||||||
|
|
||||||
|
if (!this.readNextChunk()) {
|
||||||
|
const binaryDigest = this.md5Buffer.end(true)
|
||||||
|
const base64digest = btoa(binaryDigest)
|
||||||
|
this.callback(null, base64digest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileReaderDidError(event) {
|
||||||
|
this.callback(`Error reading ${this.file.name}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
readNextChunk() {
|
||||||
|
if (this.chunkIndex < this.chunkCount) {
|
||||||
|
const start = this.chunkIndex * this.chunkSize
|
||||||
|
const end = Math.min(start + this.chunkSize, this.file.size)
|
||||||
|
const bytes = fileSlice.call(this.file, start, end)
|
||||||
|
this.fileReader.readAsArrayBuffer(bytes)
|
||||||
|
this.chunkIndex++
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
42
app/javascript/activestorage/helpers.js
Normal file
42
app/javascript/activestorage/helpers.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
export function getMetaValue(name) {
|
||||||
|
const element = findElement(document.head, `meta[name="${name}"]`)
|
||||||
|
if (element) {
|
||||||
|
return element.getAttribute("content")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findElements(root, selector) {
|
||||||
|
if (typeof root == "string") {
|
||||||
|
selector = root
|
||||||
|
root = document
|
||||||
|
}
|
||||||
|
const elements = root.querySelectorAll(selector)
|
||||||
|
return toArray(elements)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findElement(root, selector) {
|
||||||
|
if (typeof root == "string") {
|
||||||
|
selector = root
|
||||||
|
root = document
|
||||||
|
}
|
||||||
|
return root.querySelector(selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dispatchEvent(element, type, eventInit = {}) {
|
||||||
|
const { bubbles, cancelable, detail } = eventInit
|
||||||
|
const event = document.createEvent("Event")
|
||||||
|
event.initEvent(type, bubbles || true, cancelable || true)
|
||||||
|
event.detail = detail || {}
|
||||||
|
element.dispatchEvent(event)
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toArray(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value
|
||||||
|
} else if (Array.from) {
|
||||||
|
return Array.from(value)
|
||||||
|
} else {
|
||||||
|
return [].slice.call(value)
|
||||||
|
}
|
||||||
|
}
|
11
app/javascript/activestorage/index.js
Normal file
11
app/javascript/activestorage/index.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import { start } from "./ujs"
|
||||||
|
import { DirectUpload } from "./direct_upload"
|
||||||
|
export { start, DirectUpload }
|
||||||
|
|
||||||
|
function autostart() {
|
||||||
|
if (window.ActiveStorage) {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(autostart, 1)
|
74
app/javascript/activestorage/ujs.js
Normal file
74
app/javascript/activestorage/ujs.js
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { DirectUploadsController } from "./direct_uploads_controller"
|
||||||
|
import { findElement } from "./helpers"
|
||||||
|
|
||||||
|
const processingAttribute = "data-direct-uploads-processing"
|
||||||
|
let started = false
|
||||||
|
|
||||||
|
export function start() {
|
||||||
|
if (!started) {
|
||||||
|
started = true
|
||||||
|
document.addEventListener("submit", didSubmitForm)
|
||||||
|
document.addEventListener("ajax:before", didSubmitRemoteElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function didSubmitForm(event) {
|
||||||
|
handleFormSubmissionEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
function didSubmitRemoteElement(event) {
|
||||||
|
if (event.target.tagName == "FORM") {
|
||||||
|
handleFormSubmissionEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormSubmissionEvent(event) {
|
||||||
|
const form = event.target
|
||||||
|
|
||||||
|
if (form.hasAttribute(processingAttribute)) {
|
||||||
|
event.preventDefault()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new DirectUploadsController(form)
|
||||||
|
const { inputs } = controller
|
||||||
|
|
||||||
|
if (inputs.length) {
|
||||||
|
event.preventDefault()
|
||||||
|
form.setAttribute(processingAttribute, "")
|
||||||
|
inputs.forEach(disable)
|
||||||
|
controller.start(error => {
|
||||||
|
form.removeAttribute(processingAttribute)
|
||||||
|
if (error) {
|
||||||
|
inputs.forEach(enable)
|
||||||
|
} else {
|
||||||
|
submitForm(form)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitForm(form) {
|
||||||
|
let button = findElement(form, "input[type=submit]")
|
||||||
|
if (button) {
|
||||||
|
const { disabled } = button
|
||||||
|
button.disabled = false
|
||||||
|
button.click()
|
||||||
|
button.disabled = disabled
|
||||||
|
} else {
|
||||||
|
button = document.createElement("input")
|
||||||
|
button.type = "submit"
|
||||||
|
button.style = "display:none"
|
||||||
|
form.appendChild(button)
|
||||||
|
button.click()
|
||||||
|
form.removeChild(button)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disable(input) {
|
||||||
|
input.disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function enable(input) {
|
||||||
|
input.disabled = false
|
||||||
|
}
|
22
package.json
Normal file
22
package.json
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "activestorage",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Attach cloud and local files in Rails applications",
|
||||||
|
"main": "app/assets/javascripts/activestorage.js",
|
||||||
|
"repository": "git+https://github.com/rails/activestorage.git",
|
||||||
|
"author": "Javan Makhmali <javan@javan.us>",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-core": "^6.25.0",
|
||||||
|
"babel-loader": "^7.1.1",
|
||||||
|
"babel-preset-env": "^1.6.0",
|
||||||
|
"spark-md5": "^3.0.0",
|
||||||
|
"webpack": "^3.4.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"app/assets/javascripts/activestorage.js"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack -p"
|
||||||
|
}
|
||||||
|
}
|
27
webpack.config.js
Normal file
27
webpack.config.js
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
const webpack = require("webpack")
|
||||||
|
const path = require("path")
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: {
|
||||||
|
"activestorage": path.resolve(__dirname, "app/javascript/activestorage/index.js"),
|
||||||
|
},
|
||||||
|
|
||||||
|
output: {
|
||||||
|
filename: "[name].js",
|
||||||
|
path: path.resolve(__dirname, "app/assets/javascripts"),
|
||||||
|
library: "ActiveStorage",
|
||||||
|
libraryTarget: "umd"
|
||||||
|
},
|
||||||
|
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: {
|
||||||
|
loader: "babel-loader"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue