-
Notifications
You must be signed in to change notification settings - Fork 1
fix: make the standalone transform parse TypeScript and JSX #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,7 +14,8 @@ | |
| }, | ||
| "scripts": { | ||
| "build": "rimraf lib && tsc -b && cat src/types.d.ts >> lib/index.d.ts", | ||
| "dev": "tsc -w" | ||
| "dev": "tsc -w", | ||
| "test": "pnpm build && node --test test/*.test.mjs" | ||
| }, | ||
| "exports": { | ||
| "import": "./lib/index.js" | ||
|
|
@@ -23,6 +24,7 @@ | |
| "lib" | ||
| ], | ||
| "dependencies": { | ||
| "@babel/core": "^7.26.10", | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The standalone transform calls Babel directly; the old |
||
| "@babel/types": "^7.27.0", | ||
| "@compiled/babel-plugin": "^0.37.1", | ||
| "@compiled/babel-plugin-strip-runtime": "^0.37.1", | ||
|
|
@@ -35,6 +37,7 @@ | |
| }, | ||
| "devDependencies": { | ||
| "@compiled/react": "^0.18.3", | ||
| "@types/babel__core": "7.20.5", | ||
| "@types/node": "^22.14.0", | ||
| "rimraf": "^6.0.1", | ||
| "typescript": "^5.8.3", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| import t from '@babel/types'; | ||
| import babel from '@babel/core'; | ||
| import compiledPlugin from '@compiled/babel-plugin'; | ||
| import compiledStripRuntimePlugin from '@compiled/babel-plugin-strip-runtime'; | ||
| import type { ReactBabelOptions } from '@vitejs/plugin-react'; | ||
| import moduleResolverPlugin from 'babel-plugin-module-resolver'; | ||
| import { createHash } from 'crypto'; | ||
| import { EnvironmentModuleNode, type Plugin } from 'vite'; | ||
| import { createFilter, EnvironmentModuleNode, type Plugin } from 'vite'; | ||
|
|
||
| export type CompiledPluginOptions = { | ||
| /** | ||
|
|
@@ -37,8 +37,10 @@ export type CompiledPluginOptions = { | |
| }; | ||
|
|
||
| const virtualCssFiles = new Map(); | ||
| const defaultIncludeRE = /\.[tj]sx?$/; | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The published plugin inherited plugin-react's |
||
|
|
||
| export const compiled = (options: CompiledPluginOptions = {}): Plugin => { | ||
| const filter = createFilter(defaultIncludeRE); | ||
| const hash = (code: string) => { | ||
| return createHash('md5').update(code).digest('hex').substring(2, 9); | ||
| }; | ||
|
|
@@ -52,6 +54,8 @@ export const compiled = (options: CompiledPluginOptions = {}): Plugin => { | |
| let command = ''; | ||
| let root: string; | ||
| const moduleResolverPluginAlias = {}; | ||
| let plugins: babel.PluginItem[] = []; | ||
|
|
||
| return { | ||
| name: 'vite-plugin-compiled-react', | ||
| enforce: 'pre', | ||
|
|
@@ -82,6 +86,55 @@ export const compiled = (options: CompiledPluginOptions = {}): Plugin => { | |
| moduleResolverPluginAlias[find] = replacement; | ||
| } | ||
| } | ||
|
|
||
| plugins = [ | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| { | ||
| visitor: { | ||
| Program(root) { | ||
| if (/extractAssets/.test(this.filename)) { | ||
| return; | ||
| } | ||
| root.unshiftContainer('body', importDeclaration); | ||
| }, | ||
| }, | ||
| }, | ||
| [moduleResolverPlugin, { root, alias: moduleResolverPluginAlias }], | ||
| [compiledPlugin, { importReact: false, ...baseOptions }], | ||
| ]; | ||
|
|
||
| if ( | ||
| options.extract && | ||
| (options.extract === true || | ||
| (command === 'serve' && options.extract.serve) || | ||
| (command === 'build' && options.extract.build)) | ||
| ) { | ||
| plugins.push([ | ||
| compiledStripRuntimePlugin, | ||
| { compiledRequireExclude: true }, | ||
| ]); | ||
|
|
||
| plugins.push({ | ||
| visitor: { | ||
| Program: { | ||
| exit(path, { file }) { | ||
| const styleRules = file.metadata.styleRules; | ||
| if (styleRules.length) { | ||
| const code = styleRules.join('\n'); | ||
| const fileId = hash(code) + '.css'; | ||
| virtualCssFiles.set(fileId, styleRules.join('\n')); | ||
| path.unshiftContainer( | ||
| 'body', | ||
| t.importDeclaration( | ||
| [], | ||
| t.stringLiteral(`${virtualCssFileName}:${fileId}`) | ||
| ) | ||
| ); | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| }, | ||
| resolveId(source, importer, options) { | ||
| if (source.startsWith(virtualCssFileName)) { | ||
|
|
@@ -145,66 +198,43 @@ export const compiled = (options: CompiledPluginOptions = {}): Plugin => { | |
| `; | ||
| } | ||
| }, | ||
| api: { | ||
| reactBabel(babelConfig: ReactBabelOptions) { | ||
| babelConfig.plugins.push({ | ||
| visitor: { | ||
| Program(root) { | ||
| if ( | ||
| /node_modules/.test(this.filename) || | ||
| /extractAssets/.test(this.filename) | ||
| ) { | ||
| return; | ||
| } | ||
| if (/\.[jt]sx$/.test(this.filename)) { | ||
| root.unshiftContainer('body', importDeclaration); | ||
| } | ||
| }, | ||
| }, | ||
| }); | ||
| async transform(code, id) { | ||
| // Keep the same default boundary as @vitejs/plugin-react: dependencies are excluded, the | ||
| // query is stripped before filtering, and plain .js/.ts files are eligible as well. | ||
| if (id.includes('/node_modules/')) { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The old callback inherited plugin-react's dependency exclusion and query stripping. Without both here, query-suffixed source IDs are skipped and Flow dependencies reach Babel, where |
||
| return; | ||
| } | ||
| const [filepath] = id.split('?'); | ||
| if (!filepath || !filter(filepath)) { | ||
| return; | ||
| } | ||
| if ( | ||
| !filepath.endsWith('x') && | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Generated |
||
| !code.includes("'@compiled/react'") && | ||
| !code.includes('"@compiled/react"') | ||
| ) { | ||
| return; | ||
| } | ||
| const res = await babel.transformAsync(code, { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling Babel here replaces the |
||
| filename: id, | ||
| sourceFileName: filepath, | ||
| sourceMaps: true, | ||
| plugins, | ||
| // Parse only: TypeScript and JSX are left for Vite's own transform. Babel must still | ||
| // understand them, otherwise annotations and `interface` are syntax errors here. | ||
| parserOpts: { plugins: ['jsx', 'typescript'] }, | ||
| configFile: false, | ||
| babelrc: false, | ||
| }); | ||
|
|
||
| babelConfig.plugins.push([ | ||
| moduleResolverPlugin, | ||
| { root, alias: moduleResolverPluginAlias }, | ||
| ]); | ||
| babelConfig.plugins.push([ | ||
| compiledPlugin, | ||
| { importReact: false, ...baseOptions }, | ||
| ]); | ||
| if ( | ||
| options.extract && | ||
| (options.extract === true || | ||
| (command === 'serve' && options.extract.serve) || | ||
| (command === 'build' && options.extract.build)) | ||
| ) { | ||
| babelConfig.plugins.push([ | ||
| compiledStripRuntimePlugin, | ||
| { compiledRequireExclude: true }, | ||
| ]); | ||
|
|
||
| babelConfig.plugins.push({ | ||
| visitor: { | ||
| Program: { | ||
| exit(path, { file }) { | ||
| const styleRules = file.metadata.styleRules; | ||
| if (styleRules.length) { | ||
| const code = styleRules.join('\n'); | ||
| const fileId = hash(code) + '.css'; | ||
| virtualCssFiles.set(fileId, styleRules.join('\n')); | ||
| path.unshiftContainer( | ||
| 'body', | ||
| t.importDeclaration( | ||
| [], | ||
| t.stringLiteral(`${virtualCssFileName}:${fileId}`) | ||
| ) | ||
| ); | ||
| } | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| }, | ||
| if (!res || !res.code) { | ||
| return; | ||
| } | ||
|
|
||
| return { | ||
| code: res.code, | ||
| map: res.map, | ||
| }; | ||
| }, | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { before, describe, it } from 'node:test'; | ||
|
|
||
| let compiled; | ||
|
|
||
| before(async () => { | ||
| ({ compiled } = await import('../lib/index.js')); | ||
| }); | ||
|
|
||
| describe('transform filter', () => { | ||
| it('transforms plain JavaScript files', async () => { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| const result = await transform( | ||
| "import { css } from '@compiled/react'; export const value = css({ color: 'red' });", | ||
| '/project/src/style.js' | ||
| ); | ||
|
|
||
| assertCompiled(result); | ||
| }); | ||
|
|
||
| it('transforms plain TypeScript files', async () => { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Plain |
||
| const result = await transform( | ||
| "import { css } from '@compiled/react'; export const value: string = css({ color: 'red' });", | ||
| '/project/src/style.ts' | ||
| ); | ||
|
|
||
| assertCompiled(result); | ||
| }); | ||
|
|
||
| it('strips the query before filtering', async () => { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Vite appends queries to transformed IDs; filtering the full ID made valid TSX bypass Compiled. The |
||
| const result = await transform( | ||
| "export const Page = () => <div css={{ color: 'red' }} />;", | ||
| '/project/src/style.tsx?direct' | ||
| ); | ||
|
|
||
| assertCompiled(result); | ||
| assert.match(result.code, /className=\{ax/); | ||
| }); | ||
|
|
||
| it('does not transform dependencies', async () => { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Plugin-react excluded dependencies before invoking Babel. The Flow-only |
||
| const result = await transform( | ||
| "// @flow\nopaque type Token = string; export const token: Token = 'dependency';", | ||
| '/project/node_modules/flow-dependency/index.jsx' | ||
| ); | ||
|
|
||
| assert.equal(result, undefined); | ||
| }); | ||
|
|
||
| it('does not transform plain JavaScript without a Compiled import', async () => { | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The real extension build contains generated |
||
| const result = await transform( | ||
| "import { jsx } from 'react/jsx-runtime'; export const Page = () => jsx('div', {});", | ||
| '/project/generated/runtime.js' | ||
| ); | ||
|
|
||
| assert.equal(result, undefined); | ||
| }); | ||
| }); | ||
|
|
||
| async function transform(code, id) { | ||
| const plugin = compiled(); | ||
| plugin.configResolved({ root: '/project', resolve: { alias: [] } }); | ||
| return plugin.transform.call({}, code, id); | ||
| } | ||
|
|
||
| function assertCompiled(result) { | ||
| assert.ok(result); | ||
| assert.match(result.code, /generated by @compiled\/babel-plugin/); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These regressions live at the built-plugin boundary, so the test command builds
libbefore importing it. Otherwisenode:testcould exercise stale output and certify source code that consumers never load.