Selaa lähdekoodia

@uppy/core: refactor to ESM (#3744)

Antoine du Hamel 2 vuotta sitten
vanhempi
commit
456c2b7422

+ 1 - 0
.eslintrc.js

@@ -202,6 +202,7 @@ module.exports = {
         'packages/@uppy/box/src/**/*.js',
         'packages/@uppy/companion-client/src/**/*.js',
         'packages/@uppy/compressor/src/**/*.js',
+        'packages/@uppy/core/src/**/*.js',
         'packages/@uppy/dashboard/src/**/*.js',
         'packages/@uppy/drag-drop/src/**/*.js',
         'packages/@uppy/drop-target/src/**/*.js',

+ 1 - 0
__mocks__/nanoid/non-secure.js

@@ -0,0 +1 @@
+module.exports = { nanoid: () => 'cjd09qwxb000dlql4tp4doz8h' }

+ 24 - 5
bin/build-lib.js

@@ -103,6 +103,7 @@ async function buildLib () {
       }
     }
 
+    let idCounter = 0 // counter to ensure uniqueness of identifiers created by the build script.
     const plugins = await isTypeModule(file) ? [['@babel/plugin-transform-modules-commonjs', {
       importInterop: 'none',
     }], {
@@ -177,9 +178,13 @@ async function buildLib () {
         // eslint-disable-next-line no-shadow,consistent-return
         ExportNamedDeclaration (path) {
           if (path.node.source != null) {
-            if (path.node.specifiers.length !== 1 || path.node.specifiers[0].local.name !== 'default') throw new Error('unsupported export named declaration')
+            if (path.node.specifiers.length === 1
+                && path.node.specifiers[0].local.name === 'default'
+                && path.node.specifiers[0].exported.name === 'default') return ExportAllDeclaration(path)
 
-            if (path.node.specifiers[0].exported.name === 'default') return ExportAllDeclaration(path)
+            if (path.node.specifiers.some(spec => spec.exported.name === 'default')) {
+              throw new Error('unsupported mix of named and default re-exports')
+            }
 
             let { value } = path.node.source
             if (value.endsWith('.jsx') && (value.startsWith('./') || value.startsWith('../'))) {
@@ -187,6 +192,9 @@ async function buildLib () {
               value = path.node.source.value = value.slice(0, -1) // eslint-disable-line no-param-reassign,no-multi-assign
             }
 
+            // If there are no default export/import involved, Babel can handle it with no problem.
+            if (path.node.specifiers.every(spec => spec.local.name !== 'default' && spec.exported.name !== 'default')) return undefined
+
             let requireCall = t.callExpression(t.identifier('require'), [
               t.stringLiteral(value),
             ])
@@ -194,17 +202,28 @@ async function buildLib () {
               requireCall = t.logicalExpression('||', t.memberExpression(requireCall, t.identifier('default')), requireCall)
             }
 
-            const { exported } = path.node.specifiers[0]
+            const requireCallIdentifier = t.identifier(`_${idCounter++}`)
+            const namedExportIdentifiers = path.node.specifiers
+              .filter(spec => spec.local.name !== 'default')
+              .map(spec => [
+                t.identifier(requireCallIdentifier.name + spec.local.name),
+                t.memberExpression(requireCallIdentifier, spec.local),
+                spec,
+              ])
             path.insertBefore(
               t.variableDeclaration('const', [
                 t.variableDeclarator(
-                  exported,
+                  requireCallIdentifier,
                   requireCall,
                 ),
+                ...namedExportIdentifiers.map(([id, propertyAccessor]) => t.variableDeclarator(id, propertyAccessor)),
               ]),
             )
             path.replaceWith(
-              t.exportNamedDeclaration(null, [t.exportSpecifier(exported, exported)]),
+              t.exportNamedDeclaration(null, path.node.specifiers.map(spec => t.exportSpecifier(
+                spec.local.name === 'default' ? requireCallIdentifier : namedExportIdentifiers.find(([,, s]) => s === spec)[0],
+                spec.exported,
+              ))),
             )
           }
         },

+ 2 - 2
e2e/cypress/fixtures/DeepFrozenStore.js → e2e/cypress/fixtures/DeepFrozenStore.mjs

@@ -1,5 +1,5 @@
 // eslint-disable-next-line import/no-extraneous-dependencies
-const deepFreeze = require('deep-freeze')
+import deepFreeze from 'deep-freeze'
 
 /* eslint-disable no-underscore-dangle */
 
@@ -42,6 +42,6 @@ class DeepFrozenStore {
   }
 }
 
-module.exports = function defaultStore () {
+export default function defaultStore () {
   return new DeepFrozenStore()
 }

+ 4 - 0
packages/@uppy/core/package.json

@@ -6,6 +6,7 @@
   "main": "lib/index.js",
   "style": "dist/style.min.css",
   "types": "types/index.d.ts",
+  "type": "module",
   "keywords": [
     "file uploader",
     "uppy",
@@ -28,5 +29,8 @@
     "namespace-emitter": "^2.0.1",
     "nanoid": "^3.1.25",
     "preact": "^10.5.13"
+  },
+  "devDependencies": {
+    "@jest/globals": "^27.4.2"
   }
 }

+ 2 - 2
packages/@uppy/core/src/BasePlugin.js

@@ -7,9 +7,9 @@
  * See `Plugin` for the extended version with Preact rendering for interfaces.
  */
 
-const Translator = require('@uppy/utils/lib/Translator')
+import Translator from '@uppy/utils/lib/Translator'
 
-module.exports = class BasePlugin {
+export default class BasePlugin {
   constructor (uppy, opts = {}) {
     this.uppy = uppy
     this.opts = opts

+ 3 - 3
packages/@uppy/core/src/Restricter.js

@@ -1,7 +1,7 @@
 /* eslint-disable max-classes-per-file, class-methods-use-this */
 /* global AggregateError */
-const prettierBytes = require('@transloadit/prettier-bytes')
-const match = require('mime-match')
+import prettierBytes from '@transloadit/prettier-bytes'
+import match from 'mime-match'
 
 const defaultOptions = {
   maxFileSize: null,
@@ -122,4 +122,4 @@ class Restricter {
   }
 }
 
-module.exports = { Restricter, defaultOptions, RestrictionError }
+export { Restricter, defaultOptions, RestrictionError }

+ 5 - 6
packages/@uppy/core/src/UIPlugin.js

@@ -1,8 +1,8 @@
-const { render } = require('preact')
-const findDOMElement = require('@uppy/utils/lib/findDOMElement')
-const getTextDirection = require('@uppy/utils/lib/getTextDirection')
+import { render } from 'preact'
+import findDOMElement from '@uppy/utils/lib/findDOMElement'
+import getTextDirection from '@uppy/utils/lib/getTextDirection'
 
-const BasePlugin = require('./BasePlugin')
+import BasePlugin from './BasePlugin.js'
 
 /**
  * Defer a frequent call to the microtask queue.
@@ -98,7 +98,6 @@ class UIPlugin extends BasePlugin {
       this.uppy.iteratePlugins(p => {
         if (p instanceof Target) {
           targetPlugin = p
-          return false
         }
       })
     }
@@ -149,4 +148,4 @@ class UIPlugin extends BasePlugin {
   onUnmount () {}
 }
 
-module.exports = UIPlugin
+export default UIPlugin

+ 3 - 2
packages/@uppy/core/src/UIPlugin.test.js

@@ -1,5 +1,6 @@
-const UIPlugin = require('./UIPlugin')
-const Core = require('./index')
+import { describe, expect, it } from '@jest/globals'
+import UIPlugin from './UIPlugin.js'
+import Core from './index.js'
 
 describe('UIPlugin', () => {
   describe('getPluginState', () => {

+ 18 - 22
packages/@uppy/core/src/Uppy.js

@@ -1,28 +1,25 @@
 /* eslint-disable max-classes-per-file */
 /* global AggregateError */
 
-'use strict'
-
-const Translator = require('@uppy/utils/lib/Translator')
-const ee = require('namespace-emitter')
-const { nanoid } = require('nanoid/non-secure')
-const throttle = require('lodash.throttle')
-const DefaultStore = require('@uppy/store-default')
-const getFileType = require('@uppy/utils/lib/getFileType')
-const getFileNameAndExtension = require('@uppy/utils/lib/getFileNameAndExtension')
-const generateFileID = require('@uppy/utils/lib/generateFileID')
-const supportsUploadProgress = require('./supportsUploadProgress')
-const getFileName = require('./getFileName')
-const { justErrorsLogger, debugLogger } = require('./loggers')
-const {
+import Translator from '@uppy/utils/lib/Translator'
+import ee from 'namespace-emitter'
+import { nanoid } from 'nanoid/non-secure'
+import throttle from 'lodash.throttle'
+import DefaultStore from '@uppy/store-default'
+import getFileType from '@uppy/utils/lib/getFileType'
+import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension'
+import generateFileID from '@uppy/utils/lib/generateFileID'
+import supportsUploadProgress from './supportsUploadProgress.js'
+import getFileName from './getFileName.js'
+import { justErrorsLogger, debugLogger } from './loggers.js'
+import {
   Restricter,
-  defaultOptions: defaultRestrictionOptions,
+  defaultOptions as defaultRestrictionOptions,
   RestrictionError,
-} = require('./Restricter')
+} from './Restricter.js'
 
-const locale = require('./locale')
-
-// Exported from here.
+import packageJson from '../package.json'
+import locale from './locale.js'
 
 /**
  * Uppy Core module.
@@ -30,8 +27,7 @@ const locale = require('./locale')
  * adds/removes files and metadata.
  */
 class Uppy {
-  // eslint-disable-next-line global-require
-  static VERSION = require('../package.json').version
+  static VERSION = packageJson.version
 
   /** @type {Record<string, BasePlugin[]>} */
   #plugins = Object.create(null)
@@ -1559,4 +1555,4 @@ class Uppy {
   }
 }
 
-module.exports = Uppy
+export default Uppy

+ 21 - 27
packages/@uppy/core/src/Uppy.test.js

@@ -1,33 +1,27 @@
 /* eslint no-console: "off", no-restricted-syntax: "off" */
-const fs = require('fs')
-const path = require('path')
-const prettierBytes = require('@transloadit/prettier-bytes')
-const Core = require('./index')
-const UIPlugin = require('./UIPlugin')
-const AcquirerPlugin1 = require('./mocks/acquirerPlugin1')
-const AcquirerPlugin2 = require('./mocks/acquirerPlugin2')
-const InvalidPlugin = require('./mocks/invalidPlugin')
-const InvalidPluginWithoutId = require('./mocks/invalidPluginWithoutId')
-const InvalidPluginWithoutType = require('./mocks/invalidPluginWithoutType')
-const DeepFrozenStore = require('../../../../e2e/cypress/fixtures/DeepFrozenStore.js')
-
-jest.mock('nanoid/non-secure', () => {
-  return { nanoid: () => 'cjd09qwxb000dlql4tp4doz8h' }
-})
-jest.mock('@uppy/utils/lib/findDOMElement', () => {
-  return () => null
-})
+import { afterEach, beforeEach, describe, expect, it, jest, xit } from '@jest/globals'
+
+import fs from 'node:fs'
+import prettierBytes from '@transloadit/prettier-bytes'
+import Core from '../lib/index.js'
+import UIPlugin from '../lib/UIPlugin.js'
+import AcquirerPlugin1 from './mocks/acquirerPlugin1.js'
+import AcquirerPlugin2 from './mocks/acquirerPlugin2.js'
+import InvalidPlugin from './mocks/invalidPlugin.js'
+import InvalidPluginWithoutId from './mocks/invalidPluginWithoutId.js'
+import InvalidPluginWithoutType from './mocks/invalidPluginWithoutType.js'
+import DeepFrozenStore from '../../../../e2e/cypress/fixtures/DeepFrozenStore.mjs'
 
-const sampleImage = fs.readFileSync(path.join(__dirname, '../../../../e2e/cypress/fixtures/images/image.jpg'))
+const sampleImage = fs.readFileSync(new URL('../../../../e2e/cypress/fixtures/images/image.jpg', import.meta.url))
 
 describe('src/Core', () => {
-  const RealCreateObjectUrl = global.URL.createObjectURL
+  const RealCreateObjectUrl = globalThis.URL.createObjectURL
   beforeEach(() => {
-    global.URL.createObjectURL = jest.fn().mockReturnValue('newUrl')
+    globalThis.URL.createObjectURL = jest.fn().mockReturnValue('newUrl')
   })
 
   afterEach(() => {
-    global.URL.createObjectURL = RealCreateObjectUrl
+    globalThis.URL.createObjectURL = RealCreateObjectUrl
   })
 
   it('should expose a class', () => {
@@ -1098,7 +1092,7 @@ describe('src/Core', () => {
         source: 'jest',
         name: 'empty.dat',
         type: 'application/octet-stream',
-        data: new File([Buffer.alloc(1000)], { type: 'application/octet-stream' }),
+        data: new File([new Uint8Array(1000)], { type: 'application/octet-stream' }),
       })
 
       expect(core.getFiles()).toHaveLength(2)
@@ -1716,7 +1710,7 @@ describe('src/Core', () => {
       const restrictionsViolatedEventMock = jest.fn()
       const file = {
         name: 'test.jpg',
-        data: new Blob([Buffer.alloc(2 * maxFileSize)]),
+        data: new Blob([new Uint8Array(2 * maxFileSize)]),
       }
       const errorMessage = core.i18n('exceedsSize', { file: file.name, size: prettierBytes(maxFileSize) })
       try {
@@ -1766,11 +1760,11 @@ describe('src/Core', () => {
   })
 
   describe('updateOnlineStatus', () => {
-    const RealNavigatorOnline = global.window.navigator.onLine
+    const RealNavigatorOnline = globalThis.window.navigator.onLine
 
     function mockNavigatorOnline (status) {
       Object.defineProperty(
-        global.window.navigator,
+        globalThis.window.navigator,
         'onLine',
         {
           value: status,
@@ -1780,7 +1774,7 @@ describe('src/Core', () => {
     }
 
     afterEach(() => {
-      global.window.navigator.onLine = RealNavigatorOnline
+      globalThis.window.navigator.onLine = RealNavigatorOnline
     })
 
     it('should emit the correct event based on whether there is a network connection', () => {

+ 1 - 1
packages/@uppy/core/src/getFileName.js

@@ -1,4 +1,4 @@
-module.exports = function getFileName (fileType, fileDescriptor) {
+export default function getFileName (fileType, fileDescriptor) {
   if (fileDescriptor.name) {
     return fileDescriptor.name
   }

+ 18 - 10
packages/@uppy/core/src/index.js

@@ -1,12 +1,20 @@
-'use strict'
+export { default } from './Uppy.js'
+export { default as UIPlugin } from './UIPlugin.js'
+export { default as BasePlugin } from './BasePlugin.js'
+export { debugLogger } from './loggers.js'
 
-const Uppy = require('./Uppy')
-const UIPlugin = require('./UIPlugin')
-const BasePlugin = require('./BasePlugin')
-const { debugLogger } = require('./loggers')
+// TODO: remove all the following in the next major
+/* eslint-disable import/first */
+import Uppy from './Uppy.js'
+import UIPlugin from './UIPlugin.js'
+import BasePlugin from './BasePlugin.js'
+import { debugLogger } from './loggers.js'
 
-module.exports = Uppy
-module.exports.Uppy = Uppy
-module.exports.UIPlugin = UIPlugin
-module.exports.BasePlugin = BasePlugin
-module.exports.debugLogger = debugLogger
+// Backward compatibility: we want those to keep being accessible as static
+// properties of `Uppy` to avoid a breaking change.
+Uppy.Uppy = Uppy
+Uppy.UIPlugin = UIPlugin
+Uppy.BasePlugin = BasePlugin
+Uppy.debugLogger = debugLogger
+
+export { Uppy }

+ 1 - 1
packages/@uppy/core/src/locale.js

@@ -1,4 +1,4 @@
-module.exports = {
+export default {
   strings: {
     addBulkFilesFailed: {
       0: 'Failed to add %{smart_count} file due to an internal error',

+ 2 - 2
packages/@uppy/core/src/loggers.js

@@ -1,5 +1,5 @@
 /* eslint-disable no-console */
-const getTimeStamp = require('@uppy/utils/lib/getTimeStamp')
+import getTimeStamp from '@uppy/utils/lib/getTimeStamp'
 
 // Swallow all logs, except errors.
 // default if logger is not set or debug: false
@@ -17,7 +17,7 @@ const debugLogger = {
   error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
 }
 
-module.exports = {
+export {
   justErrorsLogger,
   debugLogger,
 }

+ 3 - 2
packages/@uppy/core/src/mocks/acquirerPlugin1.js

@@ -1,6 +1,7 @@
-const UIPlugin = require('../UIPlugin')
+import { jest } from '@jest/globals' // eslint-disable-line import/no-extraneous-dependencies
+import UIPlugin from '../UIPlugin.js'
 
-module.exports = class TestSelector1 extends UIPlugin {
+export default class TestSelector1 extends UIPlugin {
   constructor (uppy, opts) {
     super(uppy, opts)
     this.type = 'acquirer'

+ 3 - 2
packages/@uppy/core/src/mocks/acquirerPlugin2.js

@@ -1,6 +1,7 @@
-const UIPlugin = require('../UIPlugin')
+import { jest } from '@jest/globals' // eslint-disable-line import/no-extraneous-dependencies
+import UIPlugin from '../UIPlugin.js'
 
-module.exports = class TestSelector2 extends UIPlugin {
+export default class TestSelector2 extends UIPlugin {
   constructor (uppy, opts) {
     super(uppy, opts)
     this.type = 'acquirer'

+ 1 - 1
packages/@uppy/core/src/mocks/invalidPlugin.js

@@ -1 +1 @@
-module.exports = {}
+export default {}

+ 2 - 2
packages/@uppy/core/src/mocks/invalidPluginWithoutId.js

@@ -1,6 +1,6 @@
-const UIPlugin = require('../UIPlugin')
+import UIPlugin from '../UIPlugin.js'
 
-module.exports = class InvalidPluginWithoutName extends UIPlugin {
+export default class InvalidPluginWithoutName extends UIPlugin {
   constructor (uppy, opts) {
     super(uppy, opts)
     this.type = 'acquirer'

+ 2 - 2
packages/@uppy/core/src/mocks/invalidPluginWithoutType.js

@@ -1,6 +1,6 @@
-const UIPlugin = require('../UIPlugin')
+import UIPlugin from '../UIPlugin.js'
 
-module.exports = class InvalidPluginWithoutType extends UIPlugin {
+export default class InvalidPluginWithoutType extends UIPlugin {
   constructor (uppy, opts) {
     super(uppy, opts)
     this.id = 'InvalidPluginWithoutType'

+ 4 - 3
packages/@uppy/core/src/supportsUploadProgress.js

@@ -1,10 +1,11 @@
 // Edge 15.x does not fire 'progress' events on uploads.
 // See https://github.com/transloadit/uppy/issues/945
 // And https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12224510/
-module.exports = function supportsUploadProgress (userAgent) {
+export default function supportsUploadProgress (userAgent) {
   // Allow passing in userAgent for tests
-  if (userAgent == null) {
-    userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : null
+  if (userAgent == null && typeof navigator !== 'undefined') {
+    // eslint-disable-next-line no-param-reassign
+    userAgent = navigator.userAgent
   }
   // Assume it works because basically everything supports progress events.
   if (!userAgent) return true

+ 2 - 1
packages/@uppy/core/src/supportsUploadProgress.test.js

@@ -1,4 +1,5 @@
-const supportsUploadProgress = require('./supportsUploadProgress')
+import { describe, expect, it } from '@jest/globals'
+import supportsUploadProgress from './supportsUploadProgress.js'
 
 describe('supportsUploadProgress', () => {
   it('returns true in working browsers', () => {

+ 1 - 1
website/src/docs/core.md

@@ -280,7 +280,7 @@ const uppy = new Uppy({
 <!-- eslint-disable no-restricted-globals, no-multiple-empty-lines -->
 
 ```js
-module.exports = {
+export default {
   strings: {
     addBulkFilesFailed: {
       0: 'Failed to add %{smart_count} file due to an internal error',

+ 1 - 0
yarn.lock

@@ -9808,6 +9808,7 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@uppy/core@workspace:packages/@uppy/core"
   dependencies:
+    "@jest/globals": ^27.4.2
     "@transloadit/prettier-bytes": 0.0.7
     "@uppy/store-default": "workspace:^"
     "@uppy/utils": "workspace:^"