From 233ee252c052c187c66889e03ccffd791e46e741 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Tue, 2 May 2023 09:42:01 +1000 Subject: [PATCH] Preserve top-level `'use server'` directives like `'use client'` directives --- .changeset/silly-weeks-sparkle.md | 5 ++ packages/cli/src/build/__tests__/other.ts | 86 +++++++++++++++++++ .../src/rollup-plugins/server-components.ts | 50 +++++------ .../typescript-declarations/index.ts | 2 +- 4 files changed, 117 insertions(+), 26 deletions(-) create mode 100644 .changeset/silly-weeks-sparkle.md diff --git a/.changeset/silly-weeks-sparkle.md b/.changeset/silly-weeks-sparkle.md new file mode 100644 index 00000000..7f6d04e5 --- /dev/null +++ b/.changeset/silly-weeks-sparkle.md @@ -0,0 +1,5 @@ +--- +"@preconstruct/cli": patch +--- + +Top-level `'use server'` directives are now also preserved like `'use client'` directives. diff --git a/packages/cli/src/build/__tests__/other.ts b/packages/cli/src/build/__tests__/other.ts index 3bc7cc08..1b662338 100644 --- a/packages/cli/src/build/__tests__/other.ts +++ b/packages/cli/src/build/__tests__/other.ts @@ -1558,3 +1558,89 @@ test("simple use client with comment above directive", async () => { `); }); + +test("use server", async () => { + const dir = await testdir({ + "package.json": JSON.stringify({ + name: "pkg", + main: "dist/pkg.cjs.js", + module: "dist/pkg.esm.js", + }), + "src/index.js": js` + export { doSomething } from "./server"; + `, + "src/server.js": js` + "use server"; + export function doSomething() {} + `, + }); + await build(dir); + expect(await getFiles(dir, ["dist/**"], stripHashes("server"))) + .toMatchInlineSnapshot(` + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/pkg.cjs.dev.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + 'use strict'; + + Object.defineProperty(exports, '__esModule', { value: true }); + + var server = require('./server-some-hash.cjs.dev.js'); + + + + Object.defineProperty(exports, 'doSomething', { + enumerable: true, + get: function () { return server.doSomething; } + }); + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/pkg.cjs.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + 'use strict'; + + if (process.env.NODE_ENV === "production") { + module.exports = require("./pkg.cjs.prod.js"); + } else { + module.exports = require("./pkg.cjs.dev.js"); + } + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/pkg.cjs.prod.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + 'use strict'; + + Object.defineProperty(exports, '__esModule', { value: true }); + + var server = require('./server-some-hash.cjs.prod.js'); + + + + Object.defineProperty(exports, 'doSomething', { + enumerable: true, + get: function () { return server.doSomething; } + }); + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/pkg.esm.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + export { doSomething } from './server-some-hash.esm.js'; + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/server-this-is-not-the-real-hash-288a9f5076a34272a0270a4055aa266d.esm.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + 'use server'; + function doSomething() {} + + export { doSomething }; + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/server-this-is-not-the-real-hash-4c6b8ec0a8b072aff1b26a3ac24de144.cjs.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + 'use strict'; + + if (process.env.NODE_ENV === "production") { + module.exports = require("./server-some-hash.cjs.prod.js"); + } else { + module.exports = require("./server-some-hash.cjs.dev.js"); + } + + ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ dist/server-this-is-not-the-real-hash-f9ea3f80de7afbb1e4ac2175565ef521.cjs.dev.js, dist/server-this-is-not-the-real-hash-f9ea3f80de7afbb1e4ac2175565ef521.cjs.prod.js ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ + 'use server'; + 'use strict'; + + Object.defineProperty(exports, '__esModule', { value: true }); + + function doSomething() {} + + exports.doSomething = doSomething; + + `); +}); diff --git a/packages/cli/src/rollup-plugins/server-components.ts b/packages/cli/src/rollup-plugins/server-components.ts index 3135f43a..c9365b2a 100644 --- a/packages/cli/src/rollup-plugins/server-components.ts +++ b/packages/cli/src/rollup-plugins/server-components.ts @@ -21,7 +21,7 @@ export function serverComponentsPlugin({ } const loaded = await this.load(resolved); if ( - typeof loaded.meta.useClientReferenceId === "string" && + typeof loaded.meta.directivePreservedFile?.referenceId === "string" && importer !== undefined ) { // this name is appended for Rollup naming chunks/variables in the output @@ -29,7 +29,7 @@ export function serverComponentsPlugin({ .basename(resolved.id) .replace(/\.[tj]sx?$/, "") .replace(/[^\w]/g, "_"); - const id = `__USE_CLIENT_IMPORT__${loaded.meta.useClientReferenceId}__USE_CLIENT_IMPORT__/${name}`; + const id = `__USE_CLIENT_IMPORT__${loaded.meta.directivePreservedFile.referenceId}__USE_CLIENT_IMPORT__/${name}`; return { id, @@ -41,35 +41,35 @@ export function serverComponentsPlugin({ transform(code, id) { if (id.startsWith("\0")) return null; const directives = getModuleDirectives(code); - const useClientDirective = directives.find( - (d) => d.value === "use client" + const directive = directives.find( + (d) => d.value === "use client" || d.value === "use server" ); - if (useClientDirective) { - const magicString = new MagicString(code); - const referenceId = this.emitFile({ - type: "chunk", - id, - preserveSignature: "allow-extension", - }); - magicString.remove(useClientDirective.start, useClientDirective.end); - return { - code: magicString.toString(), - map: sourceMap - ? (magicString.generateMap({ hires: true }) as SourceMapInput) - : undefined, - meta: { - useClientReferenceId: referenceId, - }, - }; - } - return null; + if (!directive) return null; + const magicString = new MagicString(code); + const referenceId = this.emitFile({ + type: "chunk", + id, + preserveSignature: "allow-extension", + }); + magicString.remove(directive.start, directive.end); + return { + code: magicString.toString(), + map: sourceMap + ? (magicString.generateMap({ hires: true }) as SourceMapInput) + : undefined, + meta: { + directivePreservedFile: { referenceId, directive: directive.value }, + }, + }; }, renderChunk(code, chunk) { const magicString = new MagicString(code); if (chunk.facadeModuleId !== null) { const moduleInfo = this.getModuleInfo(chunk.facadeModuleId); - if (moduleInfo?.meta.useClientReferenceId) { - magicString.prepend("'use client';\n"); + if (moduleInfo?.meta.directivePreservedFile) { + const directive: "use client" | "use server" = + moduleInfo?.meta.directivePreservedFile.directive; + magicString.prepend(`'${directive}';\n`); } } diff --git a/packages/cli/src/rollup-plugins/typescript-declarations/index.ts b/packages/cli/src/rollup-plugins/typescript-declarations/index.ts index 1cdef319..4395f77a 100644 --- a/packages/cli/src/rollup-plugins/typescript-declarations/index.ts +++ b/packages/cli/src/rollup-plugins/typescript-declarations/index.ts @@ -100,7 +100,7 @@ export default function typescriptDeclarations(pkg: Package): Plugin { const moduleInfo = this.getModuleInfo(file.facadeModuleId); // this will happen for "use client" modules where it's not // an actual entrypoint but it is a Rollup entry - if (moduleInfo?.meta.useClientReferenceId) { + if (moduleInfo?.meta.directivePreservedFile) { continue; } // otherwise, a user should never be able to cause this to happen