diff --git a/.changeset/famous-radios-doubt.md b/.changeset/famous-radios-doubt.md new file mode 100644 index 00000000..6934ba06 --- /dev/null +++ b/.changeset/famous-radios-doubt.md @@ -0,0 +1,7 @@ +--- +'@wyw-in-js/transform': patch +'@wyw-in-js/rollup': patch +'wyw-in-js': patch +--- + +Plugin for Rollup. diff --git a/packages/rollup/.eslintrc.js b/packages/rollup/.eslintrc.js new file mode 100644 index 00000000..1ad149b8 --- /dev/null +++ b/packages/rollup/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@wyw-in-js/eslint-config/library'], +}; diff --git a/packages/rollup/babel.config.js b/packages/rollup/babel.config.js new file mode 100644 index 00000000..dda52988 --- /dev/null +++ b/packages/rollup/babel.config.js @@ -0,0 +1,3 @@ +const config = require('@wyw-in-js/babel-config'); + +module.exports = config; diff --git a/packages/rollup/jest.config.js b/packages/rollup/jest.config.js new file mode 100644 index 00000000..afeabf38 --- /dev/null +++ b/packages/rollup/jest.config.js @@ -0,0 +1,18 @@ +// @ts-check + +/** + * @type {import('@jest/types').Config.InitialOptions} + */ +module.exports = { + displayName: 'webpack-loader', + preset: '@wyw-in-js/jest-preset', + transform: { + '^.+\\.ts$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json', + isolatedModules: true, + }, + ], + }, +}; diff --git a/packages/rollup/package.json b/packages/rollup/package.json new file mode 100644 index 00000000..803db513 --- /dev/null +++ b/packages/rollup/package.json @@ -0,0 +1,47 @@ +{ + "name": "@wyw-in-js/rollup", + "version": "0.1.0", + "dependencies": { + "@wyw-in-js/shared": "workspace:*", + "@wyw-in-js/transform": "workspace:*", + "@rollup/pluginutils": "^5.0.4" + }, + "devDependencies": { + "@types/node": "^16.18.55", + "@wyw-in-js/babel-config": "workspace:*", + "@wyw-in-js/eslint-config": "workspace:*", + "@wyw-in-js/jest-preset": "workspace:*", + "@wyw-in-js/ts-config": "workspace:*", + "rollup": "^3.11.0", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=16.0.0" + }, + "files": [ + "esm/", + "lib/", + "types/" + ], + "license": "MIT", + "exports": { + "import": "./esm/index.js", + "require": "./lib/index.js", + "types": "./types/index.d.ts" + }, + "main": "lib/index.js", + "module": "esm/index.js", + "peerDependencies": { + "rollup": "1.20.0||^2.0.0||^3.0.0" + }, + "publishConfig": { + "access": "public" + }, + "scripts": { + "build:esm": "babel src --out-dir esm --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", + "build:lib": "cross-env NODE_ENV=legacy babel src --out-dir lib --extensions '.js,.jsx,.ts,.tsx' --source-maps --delete-dir-on-start", + "build:types": "tsc --project ./tsconfig.lib.json --baseUrl . --rootDir ./src", + "lint": "eslint --ext .js,.ts ." + }, + "types": "types/index.d.ts" +} diff --git a/packages/rollup/src/index.ts b/packages/rollup/src/index.ts new file mode 100644 index 00000000..5a775c4c --- /dev/null +++ b/packages/rollup/src/index.ts @@ -0,0 +1,145 @@ +/** + * This file contains a Rollup loader for wyw-in-js. + * It uses the transform.ts function to generate class names from source code, + * returns transformed code without template literals and attaches generated source maps + */ + +import { createFilter } from '@rollup/pluginutils'; +import type { Plugin } from 'rollup'; + +import { logger, slugify, syncResolve } from '@wyw-in-js/shared'; +import type { PluginOptions, Preprocessor, Result } from '@wyw-in-js/transform'; +import { + getFileIdx, + transform, + TransformCacheCollection, +} from '@wyw-in-js/transform'; + +type RollupPluginOptions = { + exclude?: string | string[]; + include?: string | string[]; + preprocessor?: Preprocessor; + sourceMap?: boolean; +} & Partial; + +export default function wywInJS({ + exclude, + include, + preprocessor, + sourceMap, + ...rest +}: RollupPluginOptions = {}): Plugin { + const filter = createFilter(include, exclude); + const cssLookup: { [key: string]: string } = {}; + const cache = new TransformCacheCollection(); + const emptyConfig = {}; + + const plugin: Plugin = { + name: 'wyw-in-js', + load(id: string) { + return cssLookup[id]; + }, + /* eslint-disable-next-line consistent-return */ + resolveId(importee: string) { + if (importee in cssLookup) return importee; + }, + async transform( + code: string, + id: string + ): Promise<{ code: string; map: Result['sourceMap'] } | undefined> { + // Do not transform ignored and generated files + if (!filter(id) || id in cssLookup) return; + + const log = logger.extend('rollup').extend(getFileIdx(id)); + + log('init %s', id); + + const asyncResolve = async ( + what: string, + importer: string, + stack: string[] + ) => { + const resolved = await this.resolve(what, importer); + if (resolved) { + if (resolved.external) { + // If module is marked as external, Rollup will not resolve it, + // so we need to resolve it ourselves with default resolver + const resolvedId = syncResolve(what, importer, stack); + log("resolve: ✅ '%s'@'%s -> %O\n%s", what, importer, resolved); + return resolvedId; + } + + log("resolve: ✅ '%s'@'%s -> %O\n%s", what, importer, resolved); + + // Vite adds param like `?v=667939b3` to cached modules + const resolvedId = resolved.id.split('?')[0]; + + if (resolvedId.startsWith('\0')) { + // \0 is a special character in Rollup that tells Rollup to not include this in the bundle + // https://rollupjs.org/guide/en/#outputexports + return null; + } + + return resolvedId; + } + + log("resolve: ❌ '%s'@'%s", what, importer); + throw new Error(`Could not resolve ${what}`); + }; + + const transformServices = { + options: { + filename: id, + root: process.cwd(), + preprocessor, + pluginOptions: rest, + }, + cache, + }; + + const result = await transform( + transformServices, + code, + asyncResolve, + emptyConfig + ); + + if (!result.cssText) return; + + let { cssText } = result; + + const slug = slugify(cssText); + const filename = `${id.replace(/\.[jt]sx?$/, '')}_${slug}.css`; + + if (sourceMap && result.cssSourceMapText) { + const map = Buffer.from(result.cssSourceMapText).toString('base64'); + cssText += `/*# sourceMappingURL=data:application/json;base64,${map}*/`; + } + + cssLookup[filename] = cssText; + + result.code += `\nimport ${JSON.stringify(filename)};\n`; + + /* eslint-disable-next-line consistent-return */ + return { code: result.code, map: result.sourceMap }; + }, + }; + + return new Proxy(plugin, { + get(target, prop) { + return target[prop as keyof Plugin]; + }, + + getOwnPropertyDescriptor(target, prop) { + return Object.getOwnPropertyDescriptor(target, prop as keyof Plugin); + }, + + ownKeys() { + // Rollup doesn't ask config about its own keys, so it is Vite. + + throw new Error( + 'You are trying to use @wyw-in-js/rollup with Vite. Please use @wyw-in-js/vite instead.' + ); + }, + }); +} diff --git a/packages/rollup/tsconfig.eslint.json b/packages/rollup/tsconfig.eslint.json new file mode 100644 index 00000000..b139ef3a --- /dev/null +++ b/packages/rollup/tsconfig.eslint.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "include": ["./src/**/*.ts"] +} diff --git a/packages/rollup/tsconfig.json b/packages/rollup/tsconfig.json new file mode 100644 index 00000000..1053d7a8 --- /dev/null +++ b/packages/rollup/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "@wyw-in-js/ts-config/node.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/rollup/tsconfig.lib.json b/packages/rollup/tsconfig.lib.json new file mode 100644 index 00000000..177fdab8 --- /dev/null +++ b/packages/rollup/tsconfig.lib.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./types" + }, + "exclude": ["**/__tests__/*"], + "include": ["./src/**/*.ts"] +} diff --git a/packages/rollup/tsconfig.spec.json b/packages/rollup/tsconfig.spec.json new file mode 100644 index 00000000..2ba71dfb --- /dev/null +++ b/packages/rollup/tsconfig.spec.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["**/__tests__/*"] +} diff --git a/packages/transform/src/index.ts b/packages/transform/src/index.ts index 0d06124b..a3512a3c 100644 --- a/packages/transform/src/index.ts +++ b/packages/transform/src/index.ts @@ -32,6 +32,7 @@ export { withDefaultServices } from './transform/helpers/withDefaultServices'; export type { Services } from './transform/types'; export type { EventEmitter } from './utils/EventEmitter'; export { isNode } from './utils/isNode'; +export { getFileIdx } from './utils/getFileIdx'; export { getTagProcessor } from './utils/getTagProcessor'; export { getVisitorKeys } from './utils/getVisitorKeys'; export type { VisitorKeys } from './utils/getVisitorKeys'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd81af19..7db08c95 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -255,6 +255,40 @@ importers: specifier: ^5.2.2 version: 5.2.2 + packages/rollup: + dependencies: + '@rollup/pluginutils': + specifier: ^5.0.4 + version: 5.0.4(rollup@3.29.4) + '@wyw-in-js/shared': + specifier: workspace:* + version: link:../shared + '@wyw-in-js/transform': + specifier: workspace:* + version: link:../transform + devDependencies: + '@types/node': + specifier: ^16.18.55 + version: 16.18.55 + '@wyw-in-js/babel-config': + specifier: workspace:* + version: link:../../configs/babel + '@wyw-in-js/eslint-config': + specifier: workspace:* + version: link:../../configs/eslint + '@wyw-in-js/jest-preset': + specifier: workspace:* + version: link:../../configs/jest + '@wyw-in-js/ts-config': + specifier: workspace:* + version: link:../../configs/ts + rollup: + specifier: ^3.11.0 + version: 3.29.4 + source-map: + specifier: ^0.7.4 + version: 0.7.4 + packages/shared: dependencies: debug: @@ -2633,6 +2667,21 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@rollup/pluginutils@5.0.4(rollup@3.29.4): + resolution: {integrity: sha512-0KJnIoRI8A+a1dqOYLxH8vBf8bphDmty5QvIm2hqm7oFCFYKCAZWWd2hXgMibaPsNDhI0AtpYfQZJG47pt/k4g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.2 + estree-walker: 2.0.2 + picomatch: 2.3.1 + rollup: 3.29.4 + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -5340,6 +5389,10 @@ packages: '@types/unist': 2.0.8 dev: false + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: false + /estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} dependencies: @@ -5580,7 +5633,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] requiresBuild: true - dev: true optional: true /function-bind@1.1.1: @@ -8666,7 +8718,6 @@ packages: /picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - dev: true /pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} @@ -9144,6 +9195,13 @@ packages: resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} dev: false + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + /rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} dependencies: