Repository: vanilla-extract-css/vanilla-extract (ts)

Issue:
Object literal gets duplicated when sprinkles properties are exported
### Describe the bug

### Description

When the properties passed to `createSprinkles` are exported, the webpack plugin duplicates the object literal with the properties definition (I suspect the same happens with the other plugins).

For example, given the following code:

```ts
export const responsiveProperties = defineProperties({
 // properties, shorthands, conditions, etc
});

export const colorModeProperties = defineProperties({
 // properties, shorthands, conditions, etc
});

export const sprinkles = createSprinkles(
  responsiveProperties,
  colorModeProperties,
);
```

The bundled code will end up looking like:

```js
var responsiveProperties = { /* responsiveProperties definition*/ };
var colorModeProperties = { /* colorModeProperties definition*/ };
var sprinkles = (0,_vanilla_extract_sprinkles_createRuntimeSprinkles__WEBPACK_IMPORTED_MODULE_2__.createSprinkles)({ /* responsiveProperties definition*/ },{ /* colorModeProperties definition*/ });
```

The object literals for `responsiveProperties` and `colorModeProperties` end up duplicated as arguments for `createSprinkles`.

### Steps to reproduce the bug

Check the codesandbox link with the minimal reproduction. You can also reproduce by modifying the `webpack-react` example in the `vanilla-extract` repo to export the properties in the `sprinkles.css.ts` file. In both cases, take a look at `dist/main.js` file to see how the object literals end up duplicated.


### Reproduction

https://codesandbox.io/p/sandbox/hardcore-raman-105ni0?file=%2Fsrc%2Fsprinkles.css.ts&selection=%5B%7B%22endColumn%22%3A1%2C%22endLineNumber%22%3A49%2C%22startColumn%22%3A1%2C%22startLineNumber%22%3A4%7D%5D

### System Info

```shell
System:
    OS: macOS 13.2
    CPU: (8) arm64 Apple M1 Pro
    Memory: 94.94 MB / 16.00 GB
    Shell: 5.8.1 - /bin/zsh
  Binaries:
    Node: 16.13.1 - ~/.nvm/versions/node/v16.13.1/bin/node
    Yarn: 1.22.17 - ~/.dev/yarn/v1.22.17/bin/yarn
    npm: 8.1.2 - ~/.nvm/versions/node/v16.13.1/bin/npm
    Watchman: 2023.01.30.00 - /opt/homebrew/bin/watchman
  Browsers:
    Firefox: 107.0.1
    Safari: 16.3
  npmPackages:
    @vanilla-extract/babel-plugin: 1.1.7 => 1.1.7
    @vanilla-extract/webpack-plugin: 2.1.11 => 2.1.11
    webpack: 4.46.0 => 4.46.0
```


### Used Package Manager

yarn

### Logs

_No response_

### Validations

- [X] Check that there isn't [already an issue](https://github.com/vanilla-extract-css/vanilla-extract/issues) that reports the same bug to avoid creating a duplicate.
- [X] The provided reproduction is a [minimal reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) of the bug.

Pull request description:
Allow shared variables between exports
Fixes #995 

Patch:
diff --git a/.changeset/short-students-itch.md b/.changeset/short-students-itch.md
new file mode 100644
index 0000000..5b87511
--- /dev/null
+++ b/.changeset/short-students-itch.md
@@ -0,0 +1,5 @@
+---
+'@vanilla-extract/integration': patch
+---
+
+Improve Vanilla module serialization by allowing shared variables between exports
diff --git a/packages/integration/src/processVanillaFile.ts b/packages/integration/src/processVanillaFile.ts
index 3090710..cc6c34f 100644
--- a/packages/integration/src/processVanillaFile.ts
+++ b/packages/integration/src/processVanillaFile.ts
@@ -160,10 +160,12 @@ export async function processVanillaFile({
   return serializeVanillaModule(cssImports, evalResult, unusedCompositionRegex);
 }
 
-export function stringifyExports(
-  recipeImports: Set<string>,
+function stringifyExports(
+  functionSerializationImports: Set<string>,
   value: any,
   unusedCompositionRegex: RegExp | null,
+  key: string,
+  exportLookup: Map<any, string>,
 ): any {
   return stringify(
     value,
@@ -174,13 +176,20 @@ export function stringifyExports(
         valueType === 'boolean' ||
         valueType === 'number' ||
         valueType === 'undefined' ||
-        value === null ||
-        Array.isArray(value) ||
-        isPlainObject(value)
+        value === null
       ) {
         return next(value);
       }
 
+      if (Array.isArray(value) || isPlainObject(value)) {
+        const reusedExport = exportLookup.get(value);
+
+        if (reusedExport && reusedExport !== key) {
+          return reusedExport;
+        }
+        return next(value);
+      }
+
       if (Symbol.toStringTag in Object(value)) {
         const { [Symbol.toStringTag]: _tag, ...valueWithoutTag } = value;
         return next(valueWithoutTag);
@@ -206,7 +215,7 @@ export function stringifyExports(
           typeof importName !== 'string' ||
           !Array.isArray(args)
         ) {
-          throw new Error('Invalid recipe');
+          throw new Error('Invalid function serialization params');
         }
 
         try {
@@ -215,19 +224,25 @@ export function stringifyExports(
             5,
           )}`;
 
-          recipeImports.add(
+          functionSerializationImports.add(
             `import { ${importName} as ${hashedImportName} } from '${importPath}';`,
           );
 
           return `${hashedImportName}(${args
             .map((arg) =>
-              stringifyExports(recipeImports, arg, unusedCompositionRegex),
+              stringifyExports(
+                functionSerializationImports,
+                arg,
+                unusedCompositionRegex,
+                key,
+                exportLookup,
+              ),
             )
             .join(',')})`;
         } catch (err) {
           console.error(err);
 
-          throw new Error('Invalid recipe.');
+          throw new Error('Invalid function serialization params');
         }
       }
 
@@ -246,28 +261,45 @@ export function stringifyExports(
   );
 }
 
-function serializeVanillaModule(
+const defaultExportName = '__default__';
+
+export function serializeVanillaModule(
   cssImports: Array<string>,
   exports: Record<string, unknown>,
   unusedCompositionRegex: RegExp | null,
 ) {
-  const recipeImports = new Set<string>();
-
-  const moduleExports = Object.keys(exports).map((key) =>
-    key === 'default'
-      ? `export default ${stringifyExports(
-          recipeImports,
-          exports[key],
-          unusedCompositionRegex,
-        )};`
-      : `export var ${key} = ${stringifyExports(
-          recipeImports,
-          exports[key],
-          unusedCompositionRegex,
-        )};`,
+  const functionSerializationImports = new Set<string>();
+  const exportLookup = new Map(
+    Object.entries(exports).map(([key, value]) => [
+      value,
+      key === 'default' ? defaultExportName : key,
+    ]),
   );
 
-  const outputCode = [...cssImports, ...recipeImports, ...moduleExports];
+  const moduleExports = Object.keys(exports).map((key) => {
+    const serializedExport = stringifyExports(
+      functionSerializationImports,
+      exports[key],
+      unusedCompositionRegex,
+      key === 'default' ? defaultExportName : key,
+      exportLookup,
+    );
+
+    if (key === 'default') {
+      return [
+        `var ${defaultExportName} = ${serializedExport};`,
+        `export default ${defaultExportName};`,
+      ].join('\n');
+    }
+
+    return `export var ${key} = ${serializedExport};`;
+  });
+
+  const outputCode = [
+    ...cssImports,
+    ...functionSerializationImports,
+    ...moduleExports,
+  ];
 
   return outputCode.join('\n');
 }


Test patch:
diff --git a/packages/integration/src/processVanillaFile.test.ts b/packages/integration/src/processVanillaFile.test.ts
index 3f3f7ac..9407610 100644
--- a/packages/integration/src/processVanillaFile.test.ts
+++ b/packages/integration/src/processVanillaFile.test.ts
@@ -1,11 +1,10 @@
 // @ts-expect-error
 import evalCode from 'eval';
 
-import { stringifyExports } from './processVanillaFile';
+import { serializeVanillaModule } from './processVanillaFile';
 
-describe('stringifyExports', () => {
+describe('serializeVanillaModule', () => {
   test('with plain object exports', () => {
-    const recipeImports = new Set<string>();
     const value = {
       string: 'value',
       boolean: true,
@@ -14,16 +13,19 @@ describe('stringifyExports', () => {
       object: { hello: 'world' },
     };
 
-    expect(stringifyExports(recipeImports, value, null)).toMatchInlineSnapshot(
-      `"{string:'value','boolean':true,number:42,array:[420],object:{hello:'world'}}"`,
-    );
+    expect(serializeVanillaModule([], value, null)).toMatchInlineSnapshot(`
+      "export var string = 'value';
+      export var boolean = true;
+      export var number = 42;
+      export var array = [420];
+      export var object = {hello:'world'};"
+    `);
   });
 
   // https://rollupjs.org/guide/en/#outputgeneratedcode -- see output.generatedCode.symbols
   // https://github.com/vitejs/vite/pull/5018
   // https://github.com/vitejs/vite/blob/757a92f1c7c4fa961ed963edd245df77382dfde6/packages/vite/src/node/build.ts#L466-L469
   test('with exports generated by Rollup', () => {
-    const recipeImports = new Set<string>();
     const value = Object.freeze(
       Object.defineProperty(
         {
@@ -35,13 +37,12 @@ describe('stringifyExports', () => {
       ),
     );
 
-    expect(stringifyExports(recipeImports, value, null)).toMatchInlineSnapshot(
-      `"{some:'export'}"`,
+    expect(serializeVanillaModule([], value, null)).toMatchInlineSnapshot(
+      `"export var some = 'export';"`,
     );
   });
 
   test('with unusedCompositionRegex', () => {
-    const recipeImports = new Set<string>();
     const value = {
       compositionOnly:
         'features_compositionOnly__1o6ek504 features_mergedStyle__1o6ek500 features_styleWithComposition__1o6ek501',
@@ -64,28 +65,119 @@ describe('stringifyExports', () => {
         'features_styleWithComposition__1o6ek501 features_mergedStyle__1o6ek500',
     };
 
-    const exports = stringifyExports(
-      recipeImports,
+    const exports = serializeVanillaModule(
+      [],
       value,
       /(features_compositionOnly__1o6ek504|features_styleCompositionInSelector__1o6ek507|features_styleVariantsCompositionInSelector_variant__1o6ek50a)\s/g,
     );
-    const exportsObject = evalCode(`module.exports = ${exports}`, 'dummy.js');
-    expect(exportsObject).toMatchInlineSnapshot(`
-      {
-        "compositionOnly": "features_mergedStyle__1o6ek500 features_styleWithComposition__1o6ek501",
-        "mergedStyle": "features_mergedStyle__1o6ek500",
-        "styleCompositionInSelector": "features__1o6ek505 features__1o6ek506",
-        "styleVariantsCompositionInSelector": {
-          "variant": "features__1o6ek508 features__1o6ek509",
-        },
-        "styleVariantsWithComposition": {
-          "variant": "features_styleVariantsWithComposition_variant__1o6ek502 features_mergedStyle__1o6ek500",
-        },
-        "styleVariantsWithMappedComposition": {
-          "variant": "features_styleVariantsWithMappedComposition_variant__1o6ek503 features_mergedStyle__1o6ek500",
+    expect(exports).toMatchInlineSnapshot(`
+      "export var compositionOnly = 'features_mergedStyle__1o6ek500 features_styleWithComposition__1o6ek501';
+      export var mergedStyle = 'features_mergedStyle__1o6ek500';
+      export var styleCompositionInSelector = 'features__1o6ek505 features__1o6ek506';
+      export var styleVariantsCompositionInSelector = {variant:'features__1o6ek508 features__1o6ek509'};
+      export var styleVariantsWithComposition = {variant:'features_styleVariantsWithComposition_variant__1o6ek502 features_mergedStyle__1o6ek500'};
+      export var styleVariantsWithMappedComposition = {variant:'features_styleVariantsWithMappedComposition_variant__1o6ek503 features_mergedStyle__1o6ek500'};
+      export var styleWithComposition = 'features_styleWithComposition__1o6ek501 features_mergedStyle__1o6ek500';"
+    `);
+  });
+
+  test('should serialize a vanilla module', () => {
+    expect(
+      serializeVanillaModule(
+        ['import "./styles.css"'],
+        { default: 'some-style', otherStyles: 'some-style other-style' },
+        null,
+      ),
+    ).toMatchInlineSnapshot(`
+      "import "./styles.css"
+      var __default__ = 'some-style';
+      export default __default__;
+      export var otherStyles = 'some-style other-style';"
+    `);
+  });
+
+  test('should re-use exports if possible', () => {
+    const a = {
+      style: 'the-style',
+    };
+    const b = {
+      style: 'another-style',
+    };
+    const exports = {
+      a,
+      default: b,
+      c: {
+        a,
+        b,
+      },
+    };
+
+    expect(serializeVanillaModule(['import "./styles.css"'], exports, null))
+      .toMatchInlineSnapshot(`
+      "import "./styles.css"
+      export var a = {style:'the-style'};
+      var __default__ = {style:'another-style'};
+      export default __default__;
+      export var c = {a:a,b:__default__};"
+    `);
+  });
+
+  test('should handle function serialization', () => {
+    const sprinkles = () => {};
+    sprinkles.__function_serializer__ = {
+      importPath: 'my-package',
+      importName: 'myFunction',
+      args: ['arg1', 'arg2'],
+    };
+    const exports = {
+      sprinkles,
+    };
+
+    expect(serializeVanillaModule(['import "./styles.css"'], exports, null))
+      .toMatchInlineSnapshot(`
+      "import "./styles.css"
+      import { myFunction as _86bce } from 'my-package';
+      export var sprinkles = _86bce('arg1','arg2');"
+    `);
+  });
+
+  test('should re-use exports in handle serialized function args', () => {
+    const complexExport = {
+      my: {
+        very: {
+          complex: {
+            arg: true,
+          },
         },
-        "styleWithComposition": "features_styleWithComposition__1o6ek501 features_mergedStyle__1o6ek500",
-      }
+      },
+    };
+
+    const otherComplexExport = {
+      other: {
+        complex: [1, 2, 3],
+      },
+    };
+
+    const sprinkles = () => {};
+    sprinkles.__function_serializer__ = {
+      importPath: 'my-package',
+      importName: 'myFunction',
+      args: [complexExport, otherComplexExport],
+    };
+    const exports = {
+      default: complexExport,
+      otherComplexExport,
+      sprinkles,
+    };
+
+    expect(serializeVanillaModule(['import "./styles.css"'], exports, null))
+      .toMatchInlineSnapshot(`
+      "import "./styles.css"
+      import { myFunction as _86bce } from 'my-package';
+      var __default__ = {my:{very:{complex:{arg:true}}}};
+      export default __default__;
+      export var otherComplexExport = {other:{complex:[1,2,3]}};
+      export var sprinkles = _86bce(__default__,otherComplexExport);"
     `);
   });
 });