diff --git a/.changeset/strong-suits-end.md b/.changeset/strong-suits-end.md new file mode 100644 index 00000000..12dbf977 --- /dev/null +++ b/.changeset/strong-suits-end.md @@ -0,0 +1,12 @@ +--- +'playroom': minor +--- + +Add custom entry file support + +You can provide a custom entry file via the `entry` option, which is a path to a file that runs some code before everything else. For example, if you wanted to apply a CSS reset or other global styles, polyfills etc.: + +```js +import '../path/to/your/theming-system/reset'; +import '../path/to/your/theming-system/global-styles.css'; +``` diff --git a/README.md b/README.md index e89cc6f0..b2b78ac6 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ module.exports = { // Optional: title: 'My Awesome Library', + entry: './src/entry', themes: './src/themes', snippets: './playroom/snippets.js', frameComponent: './playroom/FrameComponent.js', @@ -158,6 +159,15 @@ export default function useScope() { }; ``` +## Custom Entry + +You can provide a custom entry file via the `entry` option, which is a path to a file that runs some code before everything else. For example, if you wanted to apply a CSS reset or other global styles, polyfills etc.: + +```js +import '../path/to/your/theming-system/reset'; +import '../path/to/your/theming-system/global-styles.css'; +``` + ## Theme Support If your component library has multiple themes, you can customise Playroom to render every theme simultaneously via the `themes` configuration option. diff --git a/cypress/e2e/entry.cy.js b/cypress/e2e/entry.cy.js new file mode 100644 index 00000000..a4fceaf5 --- /dev/null +++ b/cypress/e2e/entry.cy.js @@ -0,0 +1,69 @@ +import { + assertPreviewContains, + cleanUp, + getPreviewFrames, + typeCode, +} from '../support/utils'; + +const getFirstFrame = () => + getPreviewFrames() + .first() + .then(cy.wrap) + .should( + ($frame) => expect($frame.get(0).contentDocument.body).to.not.be.empty + ); + +const assertGlobalCounter = (subject = cy.window()) => + subject.its('counter').should('equal', 1); + +describe('Entry', () => { + afterEach(() => { + cleanUp(); + }); + + describe('single entry', () => { + describe('loads the entry only once', () => { + it('for main app', () => { + cy.visit('http://localhost:9002'); + // introduce some delay to make sure everything loads + typeCode('-'); + + assertGlobalCounter(); + }); + + it('for frames', () => { + cy.visit('http://localhost:9002'); + // introduce some delay to make sure everything loads + typeCode('-'); + + assertGlobalCounter(getFirstFrame().its('0.contentWindow')); + }); + + it('for preview', () => { + cy.visit('http://localhost:9002/preview#code=N4Igxg9gJgpiBcIC0IC%2BQ'); + // wait for rendering to finish to make sure everything loads + assertPreviewContains('-'); + + assertGlobalCounter(); + }); + }); + }); + + describe('multiple entries', () => { + it('loads the entry for frames', () => { + cy.visit('http://localhost:9001/index.html'); + // introduce some delay to make sure everything loads + typeCode('-'); + + assertGlobalCounter(getFirstFrame().its('0.contentWindow')); + }); + + it('does not load the entry for main app', () => { + cy.visit('http://localhost:9001/index.html'); + // introduce some delay to make sure everything loads + typeCode('-'); + + cy.window().its('counter').should('not.exist'); + }); + }); +}); diff --git a/cypress/projects/themed/entry.js b/cypress/projects/themed/entry.js new file mode 100644 index 00000000..6346f1a5 --- /dev/null +++ b/cypress/projects/themed/entry.js @@ -0,0 +1 @@ +window.counter = 1; diff --git a/cypress/projects/themed/playroom.config.js b/cypress/projects/themed/playroom.config.js index 59b48319..a8003c11 100644 --- a/cypress/projects/themed/playroom.config.js +++ b/cypress/projects/themed/playroom.config.js @@ -1,4 +1,7 @@ module.exports = { + entry: { + frame: './entry', + }, components: './components', snippets: './snippets', themes: './themes', diff --git a/cypress/projects/typescript/entry.mjs b/cypress/projects/typescript/entry.mjs new file mode 100644 index 00000000..0fdba2d4 --- /dev/null +++ b/cypress/projects/typescript/entry.mjs @@ -0,0 +1,5 @@ +import { counter, increment } from './state.mjs'; + +export default counter; + +increment(); diff --git a/cypress/projects/typescript/playroom.config.js b/cypress/projects/typescript/playroom.config.js index 83629b91..46645435 100644 --- a/cypress/projects/typescript/playroom.config.js +++ b/cypress/projects/typescript/playroom.config.js @@ -1,4 +1,5 @@ module.exports = { + entry: './entry.mjs', components: './components.ts', snippets: './snippets.ts', outputPath: './dist', diff --git a/cypress/projects/typescript/state.mjs b/cypress/projects/typescript/state.mjs new file mode 100644 index 00000000..fd80dfc7 --- /dev/null +++ b/cypress/projects/typescript/state.mjs @@ -0,0 +1,9 @@ +/* eslint-disable no-console */ + +export let counter = 0; + +export function increment() { + window.counter = ++counter; + + console.log('incremented', window.counter); +} diff --git a/cypress/support/utils.js b/cypress/support/utils.js index 18986518..1a92b52d 100644 --- a/cypress/support/utils.js +++ b/cypress/support/utils.js @@ -168,17 +168,18 @@ export const assertPreviewContains = (text) => expect(el.get(0).innerText).to.eq(text); }); +export const cleanUp = () => + cy.window().then((win) => { + const { storageKey } = win.__playroomConfig__; + indexedDB.deleteDatabase(storageKey); + }); + export const loadPlayroom = (initialCode) => { const baseUrl = 'http://localhost:9000'; const visitUrl = initialCode ? createUrl({ baseUrl, code: dedent(initialCode) }) : baseUrl; - return cy - .visit(visitUrl) - .window() - .then((win) => { - const { storageKey } = win.__playroomConfig__; - indexedDB.deleteDatabase(storageKey); - }); + cy.visit(visitUrl); + cleanUp(); }; diff --git a/lib/defaultModules/entry.js b/lib/defaultModules/entry.js new file mode 100644 index 00000000..ad37c334 --- /dev/null +++ b/lib/defaultModules/entry.js @@ -0,0 +1 @@ +// this doesn't do anything by default diff --git a/lib/makeWebpackConfig.js b/lib/makeWebpackConfig.js index 0179abe9..fef0e160 100644 --- a/lib/makeWebpackConfig.js +++ b/lib/makeWebpackConfig.js @@ -37,12 +37,27 @@ module.exports = async (playroomConfig, options) => { const staticTypes = await getStaticTypes(playroomConfig); + const customEntries = Object.fromEntries( + ['index', 'frame', 'preview'].map((entryName) => { + const customEntry = + typeof playroomConfig.entry === 'object' + ? playroomConfig.entry[entryName] + : playroomConfig.entry; + return [ + entryName, + customEntry + ? relativeResolve(customEntry) + : require.resolve('./defaultModules/entry'), + ]; + }) + ); + const ourConfig = { mode: options.production ? 'production' : 'development', entry: { - index: [require.resolve('../src/index.js')], - frame: [require.resolve('../src/frame.js')], - preview: [require.resolve('../src/preview.js')], + index: [customEntries.index, require.resolve('../src/index.js')], + frame: [customEntries.frame, require.resolve('../src/frame.js')], + preview: [customEntries.preview, require.resolve('../src/preview.js')], }, output: { filename: '[name].[contenthash].js', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ce2a9ad..8d6b1d7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2919,7 +2919,7 @@ packages: graphemer: 1.4.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 - semver: 7.6.0 + semver: 7.3.8 tsutils: 3.21.0(typescript@5.0.4) typescript: 5.0.4 transitivePeerDependencies: @@ -2993,7 +2993,7 @@ packages: debug: 4.3.4(supports-color@8.1.1) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.0 + semver: 7.3.8 tsutils: 3.21.0(typescript@5.0.4) typescript: 5.0.4 transitivePeerDependencies: @@ -3014,7 +3014,7 @@ packages: '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.0.4) eslint: 8.44.0 eslint-scope: 5.1.1 - semver: 7.6.0 + semver: 7.3.8 transitivePeerDependencies: - supports-color - typescript @@ -4328,7 +4328,7 @@ packages: postcss-modules-scope: 3.0.0(postcss@8.4.35) postcss-modules-values: 4.0.0(postcss@8.4.35) postcss-value-parser: 4.2.0 - semver: 7.6.0 + semver: 7.3.8 webpack: 5.75.0 dev: false @@ -6812,7 +6812,7 @@ packages: jest-util: 29.3.1 natural-compare: 1.4.0 pretty-format: 29.3.1 - semver: 7.6.0 + semver: 7.3.8 transitivePeerDependencies: - supports-color dev: true @@ -6965,7 +6965,7 @@ packages: resolution: {integrity: sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==} hasBin: true dependencies: - minimist: 1.2.8 + minimist: 1.2.7 dev: true /json5@2.2.3: @@ -7415,8 +7415,12 @@ packages: resolution: {integrity: sha512-+bMdgqjMN/Z77a6NlY/I3U5LlRDbnmaAk6lDveAPKwSpcPM4tKAuYsvYF8xjhOPXhOYGe/73vVLVez5PW+jqhw==} dev: true + /minimist@1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true /mixme@0.5.4: resolution: {integrity: sha512-3KYa4m4Vlqx98GPdOHghxSdNtTvcP8E0kkaJ5Dlh+h2DRzF7zpuVVcA8B0QpKd11YJeP9QQ7ASkKzOeu195Wzw==} @@ -7427,7 +7431,7 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true dependencies: - minimist: 1.2.8 + minimist: 1.2.7 /mlly@1.5.0: resolution: {integrity: sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==} @@ -8199,7 +8203,7 @@ packages: dependencies: deep-extend: 0.6.0 ini: 1.3.8 - minimist: 1.2.8 + minimist: 1.2.7 strip-json-comments: 2.0.1 dev: true @@ -8728,12 +8732,20 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + /semver@7.3.8: + resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + /semver@7.6.0: resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} engines: {node: '>=10'} hasBin: true dependencies: lru-cache: 6.0.0 + dev: true /send@0.18.0: resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} @@ -9586,7 +9598,7 @@ packages: dependencies: '@types/json5': 0.0.29 json5: 1.0.1 - minimist: 1.2.8 + minimist: 1.2.7 strip-bom: 3.0.0 dev: true @@ -9926,7 +9938,7 @@ packages: axios: 0.25.0(debug@4.3.4) joi: 17.7.0 lodash: 4.17.21 - minimist: 1.2.8 + minimist: 1.2.7 rxjs: 7.6.0 transitivePeerDependencies: - debug diff --git a/src/index.d.ts b/src/index.d.ts index 3c32b53d..5934e4ac 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -2,6 +2,13 @@ interface PlayroomConfig { components: string; outputPath: string; title?: string; + entry?: + | string + | { + index?: string; + frame?: string; + preview?: string; + }; themes?: string; widths?: number[]; snippets?: Snippet[];