From 26fc4976e9d03bedd8d823892e5e1a0cb7eae0c1 Mon Sep 17 00:00:00 2001 From: nclsndr Date: Tue, 23 Jun 2026 10:49:04 +0200 Subject: [PATCH] chore: migrate project to ESM --- next/package.json | 1 + next/tailwind.config.ts | 11 +- next/types/tailwind-internal.d.ts | 7 ++ strapi/config/database.ts | 3 + strapi/package.json | 5 +- strapi/scripts/patch-cjs-deps-esm.js | 122 +++++++++++++++++++++ strapi/scripts/patch-strapi-esm.js | 46 ++++++++ strapi/scripts/prefillLoginFields.js | 16 ++- strapi/scripts/updateUuid.js | 8 +- strapi/src/admin/webpack.config.example.js | 6 +- strapi/tsconfig.json | 4 +- 11 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 next/types/tailwind-internal.d.ts create mode 100644 strapi/scripts/patch-cjs-deps-esm.js create mode 100644 strapi/scripts/patch-strapi-esm.js diff --git a/next/package.json b/next/package.json index 32f8c777..d4d9fd48 100644 --- a/next/package.json +++ b/next/package.json @@ -2,6 +2,7 @@ "name": "nextjs", "version": "0.1.0", "private": true, + "type": "module", "scripts": { "dev": "next dev", "build": "next build", diff --git a/next/tailwind.config.ts b/next/tailwind.config.ts index ec25b846..551c20d3 100644 --- a/next/tailwind.config.ts +++ b/next/tailwind.config.ts @@ -1,9 +1,8 @@ +import typography from '@tailwindcss/typography'; import svgToDataUri from 'mini-svg-data-uri'; import type { Config } from 'tailwindcss'; - -const { - default: flattenColorPalette, -} = require('tailwindcss/lib/util/flattenColorPalette'); +import tailwindcssAnimate from 'tailwindcss-animate'; +import flattenColorPalette from 'tailwindcss/lib/util/flattenColorPalette'; const config: Config = { content: [ @@ -50,8 +49,8 @@ const config: Config = { }, }, plugins: [ - require('tailwindcss-animate'), - require('@tailwindcss/typography'), + tailwindcssAnimate, + typography, addVariablesForColors, function ({ matchUtilities, theme }: any) { matchUtilities( diff --git a/next/types/tailwind-internal.d.ts b/next/types/tailwind-internal.d.ts new file mode 100644 index 00000000..7c48d611 --- /dev/null +++ b/next/types/tailwind-internal.d.ts @@ -0,0 +1,7 @@ +declare module 'tailwindcss/lib/util/flattenColorPalette' { + type FlattenedColors = Record; + + export default function flattenColorPalette( + colors: Record + ): FlattenedColors; +} diff --git a/strapi/config/database.ts b/strapi/config/database.ts index 7e51bf89..90494c22 100644 --- a/strapi/config/database.ts +++ b/strapi/config/database.ts @@ -1,4 +1,7 @@ import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); export default ({ env }) => { const client = env('DATABASE_CLIENT', 'sqlite'); diff --git a/strapi/package.json b/strapi/package.json index 73c6b8c8..11a81dcc 100644 --- a/strapi/package.json +++ b/strapi/package.json @@ -1,7 +1,8 @@ { "name": "strapi", "private": true, - "version": "0.1.0", + "version": "1.0.0", + "type": "module", "description": "A Strapi application", "scripts": { "develop": "strapi develop", @@ -11,7 +12,7 @@ "strapi": "strapi", "deploy": "strapi deploy", "seed": "strapi import -f ./data/export_20250116105447.tar.gz", - "postinstall": "node ./scripts/updateUuid.js" + "postinstall": "node ./scripts/updateUuid.js && node ./scripts/patch-cjs-deps-esm.js && node ./scripts/patch-strapi-esm.js" }, "devDependencies": { "@types/node": "^20", diff --git a/strapi/scripts/patch-cjs-deps-esm.js b/strapi/scripts/patch-cjs-deps-esm.js new file mode 100644 index 00000000..91721071 --- /dev/null +++ b/strapi/scripts/patch-cjs-deps-esm.js @@ -0,0 +1,122 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/* + * Why this patch exists + * --------------------- + * + * This Strapi app runs as ESM (`"type": "module"` in `strapi/package.json`) + * and compiles its server TypeScript with `module: "NodeNext"`. That makes + * Strapi load its own `.mjs` server build paths at runtime. + * + * Strapi 5.47.1 publishes ESM files, but some of those files still import + * CommonJS-only dependencies as if Node could provide native ESM named exports + * from them. Two examples seen during the migration: + * + * - `@strapi/core/dist/Strapi.mjs` imports named utilities from `lodash/fp`. + * - `@strapi/core/dist/loaders/apis.mjs` imports `existsSync` from `fs-extra`. + * + * Both packages are fundamentally CommonJS in the paths Strapi uses: + * + * - `lodash/fp` is a directory/subpath backed by `fp.js`, and Node's ESM + * resolver does not support extensionless directory imports unless the + * package provides a matching `exports` map. + * - `lodash` and `fs-extra` expose their runtime API as a CommonJS object. + * Node can provide that object as a default import, but it does not synthesize + * reliable named exports for these packages in the way Strapi's `.mjs` files + * currently expect. + * + * Without this patch, Strapi starts failing with errors such as: + * + * - `ERR_UNSUPPORTED_DIR_IMPORT: Directory import '.../lodash/fp' is not supported` + * - `SyntaxError: The requested module 'lodash/fp' does not provide an export named 'get'` + * - `SyntaxError: The requested module 'fs-extra' does not provide an export named 'existsSync'` + * + * What this script does + * --------------------- + * + * During `postinstall`, we generate tiny ESM wrapper files inside the installed + * dependency folders. Each wrapper default-imports the original CommonJS file, + * re-exports that default, and then re-exports every valid property name as a + * named ESM export. We also patch the dependency `package.json` `exports` map so + * Node resolves Strapi's imports to those wrappers. + * + * This is intentionally a local install-time compatibility shim, not a source + * code abstraction. It should be removed once Strapi publishes ESM-compatible + * imports for these dependencies or once the project upgrades to a Strapi + * version where `yarn start` works with this app's `"type": "module"` and + * `module: "NodeNext"` settings without patching `node_modules`. + */ + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const strapiRoot = path.join(__dirname, '..'); +const require = createRequire(import.meta.url); + +const isValidIdentifier = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key); + +const patchPackage = (packageName, entries) => { + const packageDir = path.join(strapiRoot, 'node_modules', packageName); + const packageJsonPath = path.join(packageDir, 'package.json'); + + if (fs.existsSync(packageJsonPath) === false) { + return; + } + + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); + const exportsMap = {}; + + for (const entry of entries) { + const cjsPath = path.join(packageDir, entry.cjsFile); + const mjsPath = path.join(packageDir, entry.mjsFile); + + if (fs.existsSync(cjsPath) === false) { + continue; + } + + const cjsModule = require(cjsPath); + const keys = Object.keys(cjsModule); + const namedExports = keys + .filter(isValidIdentifier) + .map((key) => `export const ${key} = cjsModule.${key};`) + .join('\n'); + + const importPath = `./${path + .relative(path.dirname(mjsPath), cjsPath) + .replace(/\\/g, '/')}`; + + const wrapper = `import cjsModule from '${importPath}'; + +export default cjsModule; + +${namedExports} +`; + + fs.writeFileSync(mjsPath, wrapper); + exportsMap[entry.exportSubpath] = `./${entry.mjsFile.replace(/\\/g, '/')}`; + } + + if (packageName === 'lodash') { + exportsMap['./*'] = './*.js'; + } + + if (packageName === 'fs-extra') { + exportsMap['./esm'] = './lib/esm.mjs'; + } + + packageJson.exports = exportsMap; + fs.writeFileSync( + packageJsonPath, + `${JSON.stringify(packageJson, null, 2)}\n` + ); +}; + +patchPackage('lodash', [ + { cjsFile: 'lodash.js', mjsFile: 'lodash.mjs', exportSubpath: '.' }, + { cjsFile: 'fp.js', mjsFile: 'fp.mjs', exportSubpath: './fp' }, +]); + +patchPackage('fs-extra', [ + { cjsFile: 'lib/index.js', mjsFile: 'lib/index.mjs', exportSubpath: '.' }, +]); diff --git a/strapi/scripts/patch-strapi-esm.js b/strapi/scripts/patch-strapi-esm.js new file mode 100644 index 00000000..a1184f76 --- /dev/null +++ b/strapi/scripts/patch-strapi-esm.js @@ -0,0 +1,46 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const strapiRoot = path.join(__dirname, '..'); + +const dirnamePolyfill = `import { fileURLToPath as __strapiFileURLToPath } from 'node:url'; +import { dirname as __strapiDirname } from 'node:path'; +const __dirname = __strapiDirname(__strapiFileURLToPath(import.meta.url)); +`; + +const filesToPatch = [ + 'node_modules/@strapi/core/dist/ee/license.mjs', + 'node_modules/@strapi/generators/dist/index.mjs', + 'node_modules/@strapi/admin/dist/server/server/src/routes/serve-admin-panel.mjs', + 'node_modules/@strapi/plugin-users-permissions/dist/server/register.mjs', +]; + +for (const relativePath of filesToPatch) { + const filePath = path.join(strapiRoot, relativePath); + + if (fs.existsSync(filePath) === false) { + continue; + } + + const source = fs.readFileSync(filePath, 'utf-8'); + + if (source.includes('__strapiFileURLToPath') === true) { + continue; + } + + if (source.includes('__dirname') === false) { + continue; + } + + const lastImportIndex = source.lastIndexOf('import '); + const nextLineIndex = source.indexOf('\n', lastImportIndex); + + if (lastImportIndex === -1 || nextLineIndex === -1) { + continue; + } + + const patched = `${source.slice(0, nextLineIndex + 1)}${dirnamePolyfill}${source.slice(nextLineIndex + 1)}`; + fs.writeFileSync(filePath, patched); +} diff --git a/strapi/scripts/prefillLoginFields.js b/strapi/scripts/prefillLoginFields.js index 51348296..c37af79f 100644 --- a/strapi/scripts/prefillLoginFields.js +++ b/strapi/scripts/prefillLoginFields.js @@ -1,5 +1,5 @@ -const fs = require('fs'); -const path = require('path'); +import fs from 'node:fs'; +import path from 'node:path'; // Base directory and target files const directoryPath = @@ -24,7 +24,7 @@ let found = 0; targetFiles.forEach((file) => { const filePath = path.join(directoryPath, file); - if (!fs.existsSync(filePath)) { + if (fs.existsSync(filePath) === false) { console.log(`⚠️ No matching file found for: ${file}`); return; } @@ -35,7 +35,11 @@ targetFiles.forEach((file) => { if (data.includes(newContent)) { console.log(`✅ File already modified with demo credentials: ${filePath}`); } else if (data.includes(originalContent)) { - fs.writeFileSync(filePath, data.replace(originalContent, newContent), 'utf8'); + fs.writeFileSync( + filePath, + data.replace(originalContent, newContent), + 'utf8' + ); console.log(`✅ Successfully updated: ${filePath}`); } else { console.log(`⚠️ Original content not found in: ${filePath}`); @@ -44,6 +48,8 @@ targetFiles.forEach((file) => { }); if (found === 0) { - console.error('❌ No Login.js / Login.mjs found — admin dist layout may have changed.'); + console.error( + '❌ No Login.js / Login.mjs found — admin dist layout may have changed.' + ); process.exit(1); } diff --git a/strapi/scripts/updateUuid.js b/strapi/scripts/updateUuid.js index 60944985..04364585 100644 --- a/strapi/scripts/updateUuid.js +++ b/strapi/scripts/updateUuid.js @@ -1,10 +1,12 @@ -const fs = require('fs'); -const path = require('path'); -const { v4: uuidv4 } = require('uuid'); +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { v4 as uuidv4 } from 'uuid'; // This script will update the UUID of the demo project in order to get some random analytics on // this demo usage. +const __dirname = path.dirname(fileURLToPath(import.meta.url)); const packageJsonPath = path.join(__dirname, '../package.json'); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); diff --git a/strapi/src/admin/webpack.config.example.js b/strapi/src/admin/webpack.config.example.js index 1ca45c21..0b6c8964 100644 --- a/strapi/src/admin/webpack.config.example.js +++ b/strapi/src/admin/webpack.config.example.js @@ -1,8 +1,6 @@ -'use strict'; - /* eslint-disable no-unused-vars */ -module.exports = (config, webpack) => { - // Note: we provide webpack above so you should not `require` it +export default (config, webpack) => { + // Note: we provide webpack above so you should not import it separately // Perform customizations to webpack config // Important: return the modified config return config; diff --git a/strapi/tsconfig.json b/strapi/tsconfig.json index 650b9bbf..af9f73ff 100644 --- a/strapi/tsconfig.json +++ b/strapi/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node", + "module": "NodeNext", + "moduleResolution": "NodeNext", "lib": ["ES2020"], "target": "ES2019", "strict": false,