diff --git a/eslint.config.ts b/eslint.config.ts index 680fd9b0b..44d1caea8 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -184,6 +184,12 @@ const config = tsEslint.config([ '@typescript-eslint/restrict-template-expressions': 'off', }, }, + { + files: ['**/app-react16/**/*'], + rules: { + 'react/no-deprecated': 'off', + }, + }, // must be the last config in the array // https://github.com/prettier/eslint-plugin-prettier?tab=readme-ov-file#configuration-new-eslintconfigjs prettierRecommended, diff --git a/knip.ts b/knip.ts index bb4264c9a..6d4d898e6 100644 --- a/knip.ts +++ b/knip.ts @@ -54,6 +54,7 @@ const config: KnipConfig = { // Declaring this as webpack.config instead doesn't work correctly 'config/webpack/webpack.config.js', ], + ignore: ['**/app-react16/**/*'], project: ['**/*.{js,cjs,mjs,jsx,ts,cts,mts,tsx}!', 'config/webpack/*.js'], paths: { 'Assets/*': ['client/app/assets/*'], diff --git a/script/convert b/script/convert index 7720e9567..c99b80ba2 100755 --- a/script/convert +++ b/script/convert @@ -8,26 +8,47 @@ def gsub_file_content(path, old_content, new_content) File.binwrite(path, content) end -old_config = File.expand_path("../spec/dummy/config/shakapacker.yml", __dir__) -new_config = File.expand_path("../spec/dummy/config/webpacker.yml", __dir__) +def move(old_path, new_path) + old_path = File.expand_path(old_path, __dir__) + new_path = File.expand_path(new_path, __dir__) + File.rename(old_path, new_path) +end -File.rename(old_config, new_config) +move("../spec/dummy/config/shakapacker.yml", "../spec/dummy/config/webpacker.yml") +# Shakapacker gsub_file_content("../Gemfile.development_dependencies", /gem "shakapacker", "[^"]*"/, 'gem "shakapacker", "6.6.0"') +gsub_file_content("../spec/dummy/package.json", /"shakapacker": "[^"]*",/, '"shakapacker": "6.6.0",') # The below packages don't work on the oldest supported Node version and aren't needed there anyway gsub_file_content("../package.json", /"eslint": "[^"]*",/, "") gsub_file_content("../package.json", /"globals": "[^"]*",/, "") gsub_file_content("../package.json", /"knip": "[^"]*",/, "") +gsub_file_content("../package.json", /"publint": "[^"]*",/, "") gsub_file_content("../package.json", /"typescript-eslint": "[^"]*",/, "") gsub_file_content("../package.json", %r{"@arethetypeswrong/cli": "[^"]*",}, "") gsub_file_content("../package.json", %r{"@eslint/compat": "[^"]*",}, "") gsub_file_content("../package.json", %r{"@testing-library/dom": "[^"]*",}, "") gsub_file_content("../package.json", %r{"@testing-library/react": "[^"]*",}, "") -gsub_file_content("../package.json", /"knip": "[^"]*",/, "") -gsub_file_content("../package.json", /"publint": "[^"]*",/, "") -gsub_file_content("../spec/dummy/package.json", /"shakapacker": "[^"]*",/, '"shakapacker": "6.6.0",') +# Switch to the oldest supported React version +gsub_file_content("../package.json", /"react": "[^"]*",/, '"react": "16.14.0",') +gsub_file_content("../package.json", /"react-dom": "[^"]*",/, '"react-dom": "16.14.0",') +gsub_file_content("../spec/dummy/package.json", /"react": "[^"]*",/, '"react": "16.14.0",') +gsub_file_content("../spec/dummy/package.json", /"react-dom": "[^"]*",/, '"react-dom": "16.14.0",') +gsub_file_content( + "../package.json", + "jest node_package/tests", + 'jest node_package/tests --testPathIgnorePatterns=\".*(RSC|stream|serverRenderReactComponent).*\"' +) +gsub_file_content("../tsconfig.json", "react-jsx", "react") +gsub_file_content("../spec/dummy/babel.config.js", "runtime: 'automatic'", "runtime: 'classic'") +# https://rescript-lang.org/docs/react/latest/migrate-react#configuration +gsub_file_content("../spec/dummy/rescript.json", '"version": 4', '"version": 4, "mode": "classic"') +# Find all files under app-react16 and replace the React 19 versions +Dir.glob(File.expand_path("../spec/dummy/**/app-react16/**/*.*", __dir__)).each do |file| + move(file, file.gsub("-react16", "")) +end gsub_file_content("../spec/dummy/config/webpack/commonWebpackConfig.js", /generateWebpackConfig(\(\))?/, "webpackConfig") diff --git a/spec/dummy/client/app-react16/startup/ManualRenderApp.jsx b/spec/dummy/client/app-react16/startup/ManualRenderApp.jsx new file mode 100644 index 000000000..90b8d0db2 --- /dev/null +++ b/spec/dummy/client/app-react16/startup/ManualRenderApp.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +export default (props, _railsContext, domNodeId) => { + const reactElement = ( +
+

Manual Render Example

+

If you can see this, you can register renderer functions.

+
+ ); + + const domNode = document.getElementById(domNodeId); + if (props.prerender) { + ReactDOM.hydrate(reactElement, domNode); + } else { + ReactDOM.render(reactElement, domNode); + } +}; diff --git a/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx b/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx new file mode 100644 index 000000000..b7ab77028 --- /dev/null +++ b/spec/dummy/client/app-react16/startup/ReduxApp.client.jsx @@ -0,0 +1,57 @@ +// Top level component for client side. +// Compare this to the ./ServerApp.jsx file which is used for server side rendering. +// NOTE: these are basically the same, but they are shown here + +import React from 'react'; +import { combineReducers, applyMiddleware, createStore } from 'redux'; +import { Provider } from 'react-redux'; +import thunkMiddleware from 'redux-thunk'; +import ReactDOM from 'react-dom'; + +import reducers from '../../app/reducers/reducersIndex'; +import composeInitialState from '../../app/store/composeInitialState'; + +import HelloWorldContainer from '../../app/components/HelloWorldContainer'; + +/* + * Export a function that takes the props and returns a ReactComponent. + * This is used for the client rendering hook after the page html is rendered. + * React will see that the state is the same and not do anything. + * + */ +export default (props, railsContext, domNodeId) => { + const render = props.prerender ? ReactDOM.hydrate : ReactDOM.render; + // eslint-disable-next-line no-param-reassign + delete props.prerender; + + const combinedReducer = combineReducers(reducers); + const combinedProps = composeInitialState(props, railsContext); + + // This is where we'll put in the middleware for the async function. Placeholder. + // store will have helloWorldData as a top level property + const store = createStore(combinedReducer, combinedProps, applyMiddleware(thunkMiddleware)); + + // renderApp is a function required for hot reloading. see + // https://github.com/retroalgic/react-on-rails-hot-minimal/blob/master/client/src/entry.js + + // Provider uses this.props.children, so we're not typical React syntax. + // This allows redux to add additional props to the HelloWorldContainer. + const renderApp = (Komponent) => { + const element = ( + + + + ); + + render(element, document.getElementById(domNodeId)); + }; + + renderApp(HelloWorldContainer); + + if (module.hot) { + module.hot.accept(['../reducers/reducersIndex', '../components/HelloWorldContainer'], () => { + store.replaceReducer(combineReducers(reducers)); + renderApp(HelloWorldContainer); + }); + } +}; diff --git a/spec/dummy/client/app-react16/startup/ReduxSharedStoreApp.client.jsx b/spec/dummy/client/app-react16/startup/ReduxSharedStoreApp.client.jsx new file mode 100644 index 000000000..05706772a --- /dev/null +++ b/spec/dummy/client/app-react16/startup/ReduxSharedStoreApp.client.jsx @@ -0,0 +1,45 @@ +// Top level component for the client side. +// Compare this to the ./ReduxSharedStoreApp.server.jsx file which is used for server side rendering. + +import React from 'react'; +import { Provider } from 'react-redux'; +import ReactOnRails from 'react-on-rails/client'; +import ReactDOM from 'react-dom'; + +import HelloWorldContainer from '../../app/components/HelloWorldContainer'; + +/* + * Export a function that returns a ReactComponent, depending on a store named SharedReduxStore. + * This is used for the client rendering hook after the page html is rendered. + * React will see that the state is the same and not do anything. + */ +export default (props, _railsContext, domNodeId) => { + const render = props.prerender ? ReactDOM.hydrate : ReactDOM.render; + // eslint-disable-next-line no-param-reassign + delete props.prerender; + + // This is where we get the existing store. + const store = ReactOnRails.getStore('SharedReduxStore'); + + // renderApp is a function required for hot reloading. see + // https://github.com/retroalgic/react-on-rails-hot-minimal/blob/master/client/src/entry.js + + // Provider uses this.props.children, so we're not typical React syntax. + // This allows redux to add additional props to the HelloWorldContainer. + const renderApp = (Component) => { + const element = ( + + + + ); + render(element, document.getElementById(domNodeId)); + }; + + renderApp(HelloWorldContainer); + + if (module.hot) { + module.hot.accept(['../components/HelloWorldContainer'], () => { + renderApp(HelloWorldContainer); + }); + } +};