diff --git a/package.json b/package.json index af697de109..3639f4658e 100644 --- a/package.json +++ b/package.json @@ -132,11 +132,11 @@ "@types/query-string": "~6.2", "@types/react": "^16", "@types/react-dom": "^16", - "@types/styled-components": "~4.1", "@types/react-loadable": "~5.5", "@types/react-redux": "~7.1", "@types/react-test-renderer": "~16.9", "@types/serve-static": "~1.15", + "@types/styled-components": "~4.1", "@types/tiny-async-pool": "~1.0", "@types/uuid": "^9", "@types/yargs": "~13.0", @@ -148,8 +148,8 @@ "http-proxy-middleware": "<3", "ignore-styles": "~5.0", "jest-image-snapshot": "^6", - "jest-styled-components": "^6", "jest-puppeteer": "^6", + "jest-styled-components": "^6", "less": "^4.1.3", "lighthouse": "^9", "node-fetch": "~2.6", @@ -211,11 +211,6 @@ "lcovonly" ] }, - "babelMacros": { - "styledComponents": { - "pure": true - } - }, "resolutions": { "crypto-browserify": "npm:crypto-browserify@^3.12.0" } diff --git a/script/prerender/contentPages.tsx b/script/prerender/contentPages.tsx index 8b2eddb3f1..f1299b36d1 100644 --- a/script/prerender/contentPages.tsx +++ b/script/prerender/contentPages.tsx @@ -7,7 +7,7 @@ import { renderToString } from 'react-dom/server'; import Loadable from 'react-loadable'; import { EnumChangefreq } from 'sitemap'; import { SitemapItemOptions } from 'sitemap'; -import { ServerStyleSheet, StyleSheetManager } from 'styled-components/macro'; +import { ServerStyleSheet, StyleSheetManager } from 'styled-components'; import asyncPool from 'tiny-async-pool'; import createApp from '../../src/app'; import { AppOptions } from '../../src/app'; diff --git a/src/app/components/ScrollLock.spec.tsx b/src/app/components/ScrollLock.spec.tsx index b4256387a8..a88e05b0f2 100644 --- a/src/app/components/ScrollLock.spec.tsx +++ b/src/app/components/ScrollLock.spec.tsx @@ -167,10 +167,6 @@ describe('MobileScrollLock', () => { resetModules(); ({React, renderToDom, renderer} = reactAndFriends()); - const styled = require('styled-components'); - // this is broken when unmounting without a dom - styled.createGlobalStyle = () => () => null; - MobileScrollLock = require('./ScrollLock').default; }); diff --git a/src/app/components/ScrollOffset.spec.tsx b/src/app/components/ScrollOffset.spec.tsx index bffbf4f729..192c3ec3d8 100644 --- a/src/app/components/ScrollOffset.spec.tsx +++ b/src/app/components/ScrollOffset.spec.tsx @@ -176,9 +176,6 @@ describe('ScrollOffset', () => { delete (global as any).window; resetModules(); ({React, renderer, ReactDOM, renderToDom} = reactAndFriends()); - const styled = require('styled-components'); - // this is broken when unmounting without a dom - styled.createGlobalStyle = () => () => null; ScrollOffset = require('./ScrollOffset').default; }); diff --git a/src/noStyledComponents.spec.ts b/src/noStyledComponents.spec.ts new file mode 100644 index 0000000000..63bac38c66 --- /dev/null +++ b/src/noStyledComponents.spec.ts @@ -0,0 +1,56 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +/* + * The styled-components migration (CORE-1685) is finished as far as rex-web's own code + * goes, but the package cannot be uninstalled yet: @openstax/ui-components declares it + * as a peer dependency and we render Footer, NavBar and ConfirmationToast from that + * library. See CORE-1777 / CORE-2286. + * + * That leaves styled-components resolvable from every file in the repo for as long as + * ui-components takes to migrate, with nothing stopping a new import from appearing. + * This test is the thing that stops it. When ui-components ships a styled-components-free + * release, the allowlist below goes to empty, the package comes out of package.json, and + * this file can be deleted along with it. + */ +const allowedImporters = new Set([ + // Collects the CSS that ui-components' styled components emit during prerendering, so + // the prerendered markup ships with the styles for the class names it references. + // Remove once ui-components no longer uses styled-components. + 'script/prerender/contentPages.tsx', +]); + +const searchRoots = ['src', 'script']; +const sourceExtensions = ['.ts', '.tsx']; +const skipDirectories = new Set(['node_modules', 'build', 'coverage']); + +/* + * Matches an import or a require of the package or any of its subpaths (the /macro entry + * being the one we used to use). Deliberately written not to match itself, so that this + * file does not show up in its own results. + */ +const importPattern = /(?:from|require\()\s*['"]styled-components(?:\/[^'"]*)?['"]/; + +const repoRoot = path.resolve(__dirname, '..'); + +const findSourceFiles = (directory: string): string[] => fs + .readdirSync(path.join(repoRoot, directory), {withFileTypes: true}) + .reduce((found, entry) => { + const relativePath = path.join(directory, entry.name); + + if (entry.isDirectory()) { + return skipDirectories.has(entry.name) ? found : found.concat(findSourceFiles(relativePath)); + } + + return sourceExtensions.indexOf(path.extname(entry.name)) === -1 ? found : found.concat(relativePath); + }, []); + +describe('styled-components', () => { + it('is imported only by the files that still need it', () => { + const importers = searchRoots + .reduce((found, root) => found.concat(findSourceFiles(root)), []) + .filter((relativePath) => importPattern.test(fs.readFileSync(path.join(repoRoot, relativePath), 'utf8'))); + + expect(importers.sort()).toEqual(Array.from(allowedImporters).sort()); + }); +});