Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Node.js-native ECMAScript modules #222

Merged
merged 1 commit into from
Aug 31, 2024

Conversation

andygout
Copy link
Owner

@andygout andygout commented Aug 25, 2024

This PR updates the codebase to use Node.js-native ECMAScript modules.

The decision to make this change now resulted from performing a periodic upgrade of dependencies to the latest version and discovering that the latest version of some packages now require ECMAScript modules, e.g. https://github.com/chaijs/chai/releases/tag/v5.0.0:

Chai now only supports EcmaScript Modules (ESM). This means your tests will need to either have import {...} from 'chai' or import('chai'). require('chai') will cause failures in nodejs. If you're using ESM and seeing failures, it may be due to a bundler or transpiler which is incorrectly converting import statements into require calls.

react-redux, redux, and redux-thunk also seem similarly affected.

There is a choice to remain on the currently installed version of packages like this, though it is inevitable that support will eventually stop for those versions and that upgrading will become necessary.


Codebase changes

The key change is to add to package.json:

+ "type": "module"

All import statements for native module dependencies now require the file extension, e.g.

- import routes from './routes';
+ import routes from './routes.js';

Import statements whose path pointed to a directory and relying on the default lookup of an index.js file now require that file (and its file extension) explicitly declared, e.g.

- import reducers from '../redux/reducers';
+ import reducers from '../redux/reducers/index.js';

Transpilation

Once the codebase has been changed to use ECMAScript modules, trying to run the app while there is still the presence of files with a .jsx extension will result in:

TypeError: Unknown file extension ".jsx" for /Users/andy.gout/Documents/dramatis-ssr/src/pages/Home.jsx

And switching those files' extension from jsx to .js will then result in the following error when it encounters the first piece of JSX syntax:

SyntaxError: Unexpected token '<'

While JSX files are present it is necessary to transpile the code.

JSX isn't a valid JavaScript syntax and it isn't part of any ECMAScript specification as well at the current time. NodeJS supporting ESM does not mean it supports JSX natively.

Ref. Stack Overflow: Cannot use JSX with nodejs ESM module loader

JSX is an XML-like syntax extension to ECMAScript without any defined semantics. It's NOT intended to be implemented by engines or browsers. It's NOT a proposal to incorporate JSX into the ECMAScript spec itself. It's intended to be used by various preprocessors (transpilers) to transform these tokens into standard ECMAScript.

Ref. facbook.github.io: JSX

This PR switches out Webpack for Rollup, an ECMAScript modules-first module bundler.

Consequently, all Babel-related and Webpack-related packages and code are removed (except for the introduction of the @rollup/plugin-babel package).

The following additional changes are then required:

CSS import statements whose path pointed to a file from an external package now require that the file extension is removed, e.g.

- @import 'react-bootstrap-typeahead/css/Typeahead.css';
- @import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
+ @import 'react-bootstrap-typeahead/css/Typeahead';
+ @import 'react-bootstrap-typeahead/css/Typeahead.bs5';

The thunk-middleware package requires that its default export is referenced explicitly:

- applyMiddleware(...[thunkMiddleware])
+ applyMiddleware(...[thunkMiddleware.default])

Functionality checks

Before:

  • Source maps:
  • Watch mode:
    • Server-side JS, e.g. src/server/router.js — triggers rebuild and preserves output
    • Client-side JS, e.g. src/react/pages/instances/Festival.jsx — triggers rebuild and preserves output
    • Client-side CSS, e.g. src/client/stylesheets/_navigation.scss — triggers rebuild but does not preserve output
  • Favicon

After:

  • Source maps:
  • Watch mode:
    • Server-side JS, e.g. src/server/router.js — triggers rebuild and preserves output
    • Client-side JS, e.g. src/react/pages/instances/Festival.jsx — triggers rebuild but does not preserve output (will address preserving output in subsequent branch)
    • Client-side CSS, e.g. src/client/stylesheets/_navigation.scss — triggers rebuild but does not preserve output (same as existing state; will address preserving output in subsequent branch)
    • The issue with some scenarios not being able to preserve output seems to be a side effect of the package.json start command including multiple --watch-path arguments which both change within a very short timeframe; update: the solution is to include the --watch-preserve-output flag after each --watch-path flag (rather than a single --watch-preserve-output flag after both the --watch-path flags, as this PR has implemented) — this fix will be applied in a subsequent PR (here: Preserve output for both watched paths #224)
  • Favicon

References:


New dev dependencies:

Comment on lines +15 to +36
external: [
'classnames',
'express',
'express-handlebars',
'express-session',
'morgan',
'node:http',
'node:path',
'node:url',
'prop-types',
'react',
'react-bootstrap-typeahead',
'react-dom/server',
'react-helmet',
'react-redux',
'react-router-dom',
'react-router-dom/server.js',
'redux',
'redux-thunk'
],
Copy link
Owner Author

@andygout andygout Aug 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to omit the named modules from the bundle and only include them at runtime, as detailed here: https://rollupjs.org/troubleshooting/#warning-treating-module-as-external-dependency.

It also prevents (!) Unresolved dependencies warnings in the build process:

src/server/app.js → built/main.js...
(!) Unresolved dependencies
https://rollupjs.org/troubleshooting/#warning-treating-module-as-external-dependency
node:http (imported by "src/server/app.js")
node:path (imported by "src/server/app.js")
node:url (imported by "src/server/app.js")
express (imported by "src/server/app.js", "src/server/api-router.js" and "src/server/router.js")
express-handlebars (imported by "src/server/app.js")
express-session (imported by "src/server/app.js")
morgan (imported by "src/server/app.js")
react-helmet (imported by "src/server/router.js", "src/react/Layout.jsx", "src/react/components/ErrorMessage.jsx" and "src/react/components/InstanceDocumentTitle.jsx")
react-router-dom (imported by "src/server/router.js", "src/react/AppRoutes.jsx", "src/react/Layout.jsx", "src/react/components/Header.jsx", "src/react/components/Navigation.jsx" and "src/react/components/SearchBar.jsx")
redux (imported by "src/server/router.js")
redux-thunk (imported by "src/server/router.js")
react (imported by "src/react/react-html.jsx", "src/react/AppRoutes.jsx", "src/react/pages/NotFound.jsx", "src/react/pages/Home.jsx", "src/react/Layout.jsx", "src/react/pages/instances/FestivalSeries.jsx", "src/react/pages/instances/Festival.jsx", "src/react/pages/instances/Person.jsx", "src/react/pages/lists/Companies.jsx", "src/react/pages/lists/Materials.jsx", "src/react/pages/instances/Award.jsx", "src/react/pages/instances/Venue.jsx", "src/react/pages/instances/Company.jsx", "src/react/pages/lists/People.jsx", "src/react/pages/lists/Productions.jsx", "src/react/pages/instances/AwardCeremony.jsx", "src/react/pages/lists/Venues.jsx", "src/react/pages/lists/Characters.jsx", "src/react/pages/lists/Seasons.jsx", "src/react/pages/lists/Awards.jsx", "src/react/pages/instances/Material.jsx", "src/react/pages/lists/Festivals.jsx", "src/react/pages/lists/AwardCeremonies.jsx", "src/react/pages/instances/Character.jsx", "src/react/pages/instances/Season.jsx", "src/react/pages/lists/FestivalSerieses.jsx", "src/react/pages/instances/Production.jsx", "src/react/components/Header.jsx", "src/react/components/FormattedJson.jsx", "src/react/components/ErrorMessage.jsx", "src/react/components/InstanceLabel.jsx", "src/react/components/Footer.jsx", "src/react/components/Navigation.jsx", "src/react/components/Notification.jsx", "src/react/components/InstanceDocumentTitle.jsx", "src/react/wrappers/ListWrapper.jsx", "src/react/components/PageTitle.jsx", "src/react/components/instance-forms/CompanyForm.jsx", "src/react/components/instance-forms/FestivalForm.jsx", "src/react/components/withInstancePageTitle.jsx", "src/react/components/instance-forms/FestivalSeriesForm.jsx", "src/react/components/instance-forms/AwardForm.jsx", "src/react/wrappers/InstanceWrapper.jsx", "src/react/components/ScrollToTop.jsx", "src/react/components/instance-forms/VenueForm.jsx", "src/react/components/SearchBar.jsx", "src/react/components/instance-forms/CharacterForm.jsx", "src/react/components/instance-forms/PersonForm.jsx", "src/react/components/instance-forms/AwardCeremonyForm.jsx", "src/react/components/instance-forms/SeasonForm.jsx", "src/react/components/instance-forms/ProductionForm.jsx", "src/react/components/instance-forms/MaterialForm.jsx", "src/react/components/form/ArrayItemActionButton.jsx", "src/react/components/form/InputAndErrors.jsx", "src/react/components/form/InputErrors.jsx", "src/react/components/form/FieldsetComponent.jsx", "src/react/components/form/Input.jsx", "src/react/components/form/FormWrapper.jsx" and "src/react/components/form/Fieldset.jsx")
react-dom/server (imported by "src/react/react-html.jsx")
react-redux (imported by "src/react/react-html.jsx", "src/react/Layout.jsx", "src/react/pages/instances/FestivalSeries.jsx", "src/react/pages/instances/Festival.jsx", "src/react/pages/instances/Person.jsx", "src/react/pages/lists/Companies.jsx", "src/react/pages/lists/Materials.jsx", "src/react/pages/instances/Award.jsx", "src/react/pages/instances/Venue.jsx", "src/react/pages/instances/Company.jsx", "src/react/pages/lists/People.jsx", "src/react/pages/lists/Productions.jsx", "src/react/pages/instances/AwardCeremony.jsx", "src/react/pages/lists/Venues.jsx", "src/react/pages/lists/Characters.jsx", "src/react/pages/lists/Seasons.jsx", "src/react/pages/lists/Awards.jsx", "src/react/pages/instances/Material.jsx", "src/react/pages/lists/Festivals.jsx", "src/react/pages/lists/AwardCeremonies.jsx", "src/react/pages/instances/Character.jsx", "src/react/pages/instances/Season.jsx", "src/react/pages/lists/FestivalSerieses.jsx", "src/react/pages/instances/Production.jsx" and "src/react/components/form/FormWrapper.jsx")
react-router-dom/server.js (imported by "src/react/react-html.jsx")
prop-types (imported by "src/react/Layout.jsx", "src/react/pages/instances/FestivalSeries.jsx", "src/react/pages/instances/Festival.jsx", "src/react/pages/instances/Person.jsx", "src/react/pages/lists/Companies.jsx", "src/react/pages/lists/Materials.jsx", "src/react/pages/instances/Award.jsx", "src/react/pages/instances/Venue.jsx", "src/react/pages/instances/Company.jsx", "src/react/pages/lists/People.jsx", "src/react/pages/lists/Productions.jsx", "src/react/pages/instances/AwardCeremony.jsx", "src/react/pages/lists/Venues.jsx", "src/react/pages/lists/Characters.jsx", "src/react/pages/lists/Seasons.jsx", "src/react/pages/lists/Awards.jsx", "src/react/pages/instances/Material.jsx", "src/react/pages/lists/Festivals.jsx", "src/react/pages/lists/AwardCeremonies.jsx", "src/react/pages/instances/Character.jsx", "src/react/pages/instances/Season.jsx", "src/react/pages/lists/FestivalSerieses.jsx", "src/react/pages/instances/Production.jsx", "src/react/components/FormattedJson.jsx", "src/react/components/ErrorMessage.jsx", "src/react/components/InstanceLabel.jsx", "src/react/components/Notification.jsx", "src/react/components/InstanceDocumentTitle.jsx", "src/react/wrappers/ListWrapper.jsx", "src/react/components/PageTitle.jsx", "src/react/components/instance-forms/CompanyForm.jsx", "src/react/components/instance-forms/FestivalForm.jsx", "src/react/components/withInstancePageTitle.jsx", "src/react/components/instance-forms/FestivalSeriesForm.jsx", "src/react/components/instance-forms/AwardForm.jsx", "src/react/wrappers/InstanceWrapper.jsx", "src/react/components/instance-forms/VenueForm.jsx", "src/react/components/instance-forms/CharacterForm.jsx", "src/react/components/instance-forms/PersonForm.jsx", "src/react/components/instance-forms/AwardCeremonyForm.jsx", "src/react/components/instance-forms/SeasonForm.jsx", "src/react/components/instance-forms/ProductionForm.jsx", "src/react/components/instance-forms/MaterialForm.jsx", "src/react/components/form/ArrayItemActionButton.jsx", "src/react/components/form/InputAndErrors.jsx", "src/react/components/form/InputErrors.jsx", "src/react/components/form/FieldsetComponent.jsx", "src/react/components/form/Input.jsx", "src/react/components/form/FormWrapper.jsx" and "src/react/components/form/Fieldset.jsx")
classnames (imported by "src/react/components/Notification.jsx", "src/react/components/PageTitle.jsx", "src/react/components/form/FieldsetComponent.jsx" and "src/react/components/form/Input.jsx")
react-bootstrap-typeahead (imported by "src/react/components/SearchBar.jsx")

Comment on lines +35 to +39
watch: {
clearScreen: false
},
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whether to clear the screen when a rebuild is triggered.

Ref. https://rollupjs.org/configuration-options/#watch-clearscreen

Comment on lines +50 to +78
const clientScriptsBundle = {
input: 'src/react/client-mount.jsx',
output: {
file: 'public/main.js',
format: 'iife'
},
watch: {
clearScreen: false
},
plugins: [
nodeResolve({
browser: true,
extensions: ['.js', '.jsx']
}),
babel({
babelHelpers: 'bundled',
presets: ['@babel/preset-react'],
extensions: ['.js', '.jsx']
}),
commonjs(),
replace({
preventAssignment: false,
'process.env.NODE_ENV': JSON.stringify('development')
})
]
};
Copy link
Owner Author

@andygout andygout Aug 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on lines -17 to +18
@import 'react-bootstrap-typeahead/css/Typeahead.css';
@import 'react-bootstrap-typeahead/css/Typeahead.bs5.css';
@import 'react-bootstrap-typeahead/css/Typeahead';
@import 'react-bootstrap-typeahead/css/Typeahead.bs5';
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without these changes, these imported files will be omitted from the transpiled CSS file.

applyMiddleware(...[thunkMiddleware])
applyMiddleware(...[thunkMiddleware.default])
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without this change, the following error will occur when the app is run:

/{path}/dramatis-cms/node_modules/redux/lib/redux.js:162
    return enhancer(createStore)(reducer, preloadedState);
                                ^
TypeError: middleware is not a function
    at /{path}/dramatis-cms/node_modules/redux/lib/redux.js:703:16
    at Array.map (<anonymous>)
    at /{path}/dramatis-cms/node_modules/redux/lib/redux.js:702:31
    at createStore (/{path}/dramatis-cms/node_modules/redux/lib/redux.js:162:33)
    at file:///{path}/dramatis-cms/src/server/router.js:13:15
    at ModuleJob.run (node:internal/modules/esm/module_job:262:25)
    at ModuleLoader.import (node:internal/modules/esm/loader:475:24)
    at asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:109:5)
Failed running 'built/main.js'

@andygout andygout force-pushed the use-nodejs-native-ecmascript-modules branch 2 times, most recently from ded76e2 to 743488b Compare August 25, 2024 17:21
rollup.config.js Outdated
Comment on lines 39 to 49
esbuild({
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment'
})
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The properties for the object provided as an argument are taken from this usage example https://github.com/egoist/rollup-plugin-esbuild#usage, though for many properties the default value is appropriate.

import esbuild from 'rollup-plugin-esbuild'

export default {
  plugins: [
    esbuild({
      // All options are optional
      include: /\.[jt]sx?$/, // default, inferred from `loaders` option
      exclude: /node_modules/, // default
      sourceMap: true, // default
      minify: process.env.NODE_ENV === 'production',
      target: 'es2017', // default, or 'es20XX', 'esnext'
      jsx: 'transform', // default, or 'preserve'
      jsxFactory: 'React.createElement',
      jsxFragment: 'React.Fragment',
      // Like @rollup/plugin-replace
      define: {
        __VERSION__: '"x.y.z"',
      },
      tsconfig: 'tsconfig.json', // default
      // Add extra loaders
      loaders: {
        // Add .json files support
        // require @rollup/plugin-commonjs
        '.json': 'json',
        // Enable JSX in .js files too
        '.js': 'jsx',
      },
    }),
  ],
}

@andygout andygout force-pushed the use-nodejs-native-ecmascript-modules branch from 743488b to 338ab78 Compare August 26, 2024 13:49
"build": "webpack",
"watch": "webpack --watch",
"build": "rollup --config",
"watch": "rollup --config --watch",
Copy link
Owner Author

@andygout andygout Aug 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

watch command requires --config flag prior to --watch flag.

Ref. Medium: The Ultimate Guide to Getting Started with the Rollup.js JavaScript Bundler by Craig Buckler

@andygout andygout force-pushed the use-nodejs-native-ecmascript-modules branch from 338ab78 to fa331dd Compare August 31, 2024 09:06
@andygout andygout force-pushed the use-nodejs-native-ecmascript-modules branch from fa331dd to 47e29eb Compare August 31, 2024 09:45
@andygout andygout merged commit e41f990 into main Aug 31, 2024
1 check passed
@andygout andygout deleted the use-nodejs-native-ecmascript-modules branch August 31, 2024 10:08
This was referenced Dec 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant