From 651345afa80d9868c21611eb1561cf4030a3c612 Mon Sep 17 00:00:00 2001
From: tianyingchun <tianyingchun@outlook.com>
Date: Fri, 21 Jun 2024 10:08:13 +0800
Subject: [PATCH] feat: add `.env.*` copy support

---
 .changeset/proud-parents-rescue.md |  5 ++
 package.json                       |  1 +
 src/file-walk.ts                   | 16 +++++
 src/next-standalone.ts             | 10 ++++
 tests/file-walk.spec.ts            | 96 ++++++++++++++++++++++++++++++
 tests/next-standalone.spec.ts      | 37 +++++++++++-
 yarn.lock                          | 40 ++++++++++++-
 7 files changed, 202 insertions(+), 3 deletions(-)
 create mode 100644 .changeset/proud-parents-rescue.md
 create mode 100644 src/file-walk.ts
 create mode 100644 tests/file-walk.spec.ts

diff --git a/.changeset/proud-parents-rescue.md b/.changeset/proud-parents-rescue.md
new file mode 100644
index 0000000..f3e20af
--- /dev/null
+++ b/.changeset/proud-parents-rescue.md
@@ -0,0 +1,5 @@
+---
+"@hyperse/hyper-env": patch
+---
+
+add `.env.*` copy support
diff --git a/package.json b/package.json
index 4ec460b..7c95a31 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
     "@vercel/nft": "^0.27.2",
     "dotenv": "^16.4.5",
     "dotenv-expand": "^11.0.6",
+    "globby": "^14.0.1",
     "minimist": "^1.2.8"
   },
   "devDependencies": {
diff --git a/src/file-walk.ts b/src/file-walk.ts
new file mode 100644
index 0000000..e2339c6
--- /dev/null
+++ b/src/file-walk.ts
@@ -0,0 +1,16 @@
+import type { Options } from 'globby';
+import { globby } from 'globby';
+
+export const fileWalk = (
+  pattern: string | readonly string[],
+  options: Options = {}
+): Promise<string[]> => {
+  const ignorePattern = options.ignore || [];
+  return globby(pattern, {
+    absolute: false,
+    dot: true,
+    unique: true,
+    ...options,
+    ignore: [...ignorePattern, '**/__MACOSX/**', '**/*.DS_Store'],
+  });
+};
diff --git a/src/next-standalone.ts b/src/next-standalone.ts
index b908b8b..e8635b8 100644
--- a/src/next-standalone.ts
+++ b/src/next-standalone.ts
@@ -3,6 +3,7 @@ import fsPromise from 'fs/promises';
 import minimist from 'minimist';
 import { dirname, resolve } from 'path';
 import { nodeFileTrace } from '@vercel/nft';
+import { fileWalk } from './file-walk.js';
 import { getDirname } from './get-dir-name.js';
 
 type Argv = {
@@ -38,6 +39,15 @@ export const nextStandalone = async (args: string[]) => {
     base: fromBase,
   });
 
+  const envFiles = await fileWalk(['.env', '.env.*'], {
+    cwd: fromBase,
+    absolute: false,
+  });
+
+  for (const absEnvFile of envFiles) {
+    fileList.add(absEnvFile);
+  }
+
   for (const filePath of fileList) {
     const copyTo = resolve(copyToBase, '.next/standalone', filePath);
     if (!fs.existsSync(dirname(copyTo))) {
diff --git a/tests/file-walk.spec.ts b/tests/file-walk.spec.ts
new file mode 100644
index 0000000..e616629
--- /dev/null
+++ b/tests/file-walk.spec.ts
@@ -0,0 +1,96 @@
+import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileWalk } from '../src/file-walk.js';
+import { getDirname } from '../src/get-dir-name.js';
+
+const createFixtureFiles = (
+  url: string,
+  dir = 'fixture',
+  files: Record<string, string>
+) => {
+  const fixtureCwd = getDirname(url, dir);
+  mkdirSync(fixtureCwd, {
+    recursive: true,
+  });
+  for (const [key, value] of Object.entries(files)) {
+    const item = join(fixtureCwd, key);
+    mkdirSync(dirname(item), {
+      recursive: true,
+    });
+    writeFileSync(item, value ? value : 'hello' + Math.random());
+  }
+  return fixtureCwd;
+};
+
+describe('fileWalk', () => {
+  let fixtureCwd: string;
+  const envFiles = {
+    '.env': '',
+    '.env.dev': '',
+    '.env.inte': '',
+    '.env.rc': '',
+    '.env.prod': '',
+  };
+  beforeAll(() => {
+    fixtureCwd = createFixtureFiles(import.meta.url, 'filewalk', {
+      'a/b/c/text.txt': '',
+      'a/b/c/image.jpg': '',
+      'a/b/c/image.png': '',
+      'a/b/c/d/e/image.jpg': '',
+      'a/b/c/d/e/image.png': '',
+      'a/b/c/style.css': '',
+      'a/b/c/.gitignore': '',
+      '__MACOSX/test/._demo-8ca86e6b.png': '',
+      '__MACOSX/test/demo-8ca86e6b.png': '',
+      '__MACOSX/test/assets/__MACOSX/test/._.DS_Store': '',
+      'abc/__MACOSX/test/._demo-8ca86e6b.png': '',
+      'abc/__MACOSX/test/demo-8ca86e6b.png': '',
+      'abc/__MACOSX/test/assets/__MACOSX/test/._.DS_Store': '',
+      ...envFiles,
+    });
+  });
+
+  afterAll(() => {
+    rmSync(fixtureCwd, {
+      force: true,
+      recursive: true,
+    });
+  });
+
+  describe('fileWalkAsync', () => {
+    it('should asynchronously support correct globby patterns & negative patterns', async () => {
+      const files = await fileWalk('**/*.*', {
+        cwd: fixtureCwd,
+        ignore: ['**/*.{jpg,png}'],
+      });
+      expect(files.length).toBe(8);
+      expect(files.filter((s) => s.endsWith('.gitignore')).length).toBe(1);
+    });
+
+    it('should asynchronously currect handle dot files', async () => {
+      const files = await fileWalk('**/*.*', {
+        cwd: fixtureCwd,
+      });
+      expect(files.length).toBe(12);
+      expect(files.filter((s) => s.endsWith('.gitignore')).length).toBe(1);
+    });
+
+    it('should asynchronously ignore __MACOSX & .DS_Store', async () => {
+      const files = await fileWalk('**/*.*', {
+        cwd: fixtureCwd,
+      });
+      expect(files.length).toBe(12);
+      expect(files.filter((s) => s.endsWith('.gitignore')).length).toBe(1);
+    });
+
+    it('should asynchronously list .env fules', async () => {
+      const files = await fileWalk(['.env', '.env.*'], {
+        cwd: fixtureCwd,
+      });
+      expect(files.length).toBe(5);
+      for (const [expectEnvFile] of Object.entries(envFiles)) {
+        expect(files.find((s) => s.endsWith(expectEnvFile))).toBeDefined();
+      }
+    });
+  });
+});
diff --git a/tests/next-standalone.spec.ts b/tests/next-standalone.spec.ts
index 8e9a016..4a84205 100644
--- a/tests/next-standalone.spec.ts
+++ b/tests/next-standalone.spec.ts
@@ -1,11 +1,31 @@
-import fs, { rmdirSync } from 'fs';
+import fs, { rmdirSync, rmSync, writeFileSync } from 'fs';
 import fsPromise from 'fs/promises';
 import { join } from 'path';
 import { getDirname } from '../src/get-dir-name.js';
 import { nextStandalone } from '../src/next-standalone.js';
 
 describe('Next Standalone', () => {
+  const fixtureCwd = getDirname(import.meta.url);
   const binFile = getDirname(import.meta.url, '../bin/hyper-env.mjs');
+  const envFiles = {
+    '.env': '',
+    '.env.dev': '',
+    '.env.inte': '',
+    '.env.rc': '',
+    '.env.prod': '',
+  };
+
+  beforeAll(() => {
+    for (const [envFile] of Object.entries(envFiles)) {
+      writeFileSync(join(fixtureCwd, envFile), '');
+    }
+  });
+
+  afterAll(() => {
+    for (const [envFile] of Object.entries(envFiles)) {
+      rmSync(join(fixtureCwd, envFile));
+    }
+  });
 
   beforeEach(() => {
     vi.spyOn(fs, 'existsSync').mockReturnValue(true);
@@ -71,6 +91,21 @@ describe('Next Standalone', () => {
     }
   });
 
+  it('should correct handle copy .env files for workdir', async () => {
+    await nextStandalone([
+      '--fromBase',
+      fixtureCwd,
+      '--copyToBase',
+      fixtureCwd,
+    ]);
+    for (const envFile of Object.keys(envFiles)) {
+      expect(fsPromise.copyFile).toHaveBeenCalledWith(
+        join(fixtureCwd, envFile),
+        join(fixtureCwd, '.next/standalone', envFile)
+      );
+    }
+  });
+
   it('should correct handle argv dummy standard parameters', async () => {
     const fromBase = '/fromBase';
     const copyToBase = '/copyToBase';
diff --git a/yarn.lock b/yarn.lock
index 6dced67..475ebfd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -822,6 +822,7 @@ __metadata:
     dotenv: "npm:^16.4.5"
     dotenv-expand: "npm:^11.0.6"
     eslint: "npm:^9.5.0"
+    globby: "npm:^14.0.1"
     husky: "npm:9.0.11"
     lint-staged: "npm:15.2.7"
     minimist: "npm:^1.2.8"
@@ -1285,6 +1286,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@sindresorhus/merge-streams@npm:^2.1.0":
+  version: 2.3.0
+  resolution: "@sindresorhus/merge-streams@npm:2.3.0"
+  checksum: 10/798bcb53cd1ace9df84fcdd1ba86afdc9e0cd84f5758d26ae9b1eefd8e8887e5fc30051132b9e74daf01bb41fa5a2faf1369361f83d76a3b3d7ee938058fd71c
+  languageName: node
+  linkType: hard
+
 "@sindresorhus/merge-streams@npm:^4.0.0":
   version: 4.0.0
   resolution: "@sindresorhus/merge-streams@npm:4.0.0"
@@ -3977,7 +3985,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"fast-glob@npm:^3.2.5, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0":
+"fast-glob@npm:^3.2.5, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.2":
   version: 3.3.2
   resolution: "fast-glob@npm:3.3.2"
   dependencies:
@@ -4513,6 +4521,20 @@ __metadata:
   languageName: node
   linkType: hard
 
+"globby@npm:^14.0.1":
+  version: 14.0.1
+  resolution: "globby@npm:14.0.1"
+  dependencies:
+    "@sindresorhus/merge-streams": "npm:^2.1.0"
+    fast-glob: "npm:^3.3.2"
+    ignore: "npm:^5.2.4"
+    path-type: "npm:^5.0.0"
+    slash: "npm:^5.1.0"
+    unicorn-magic: "npm:^0.1.0"
+  checksum: 10/b36f57afc45a857a884d82657603c7e1663b1e6f3f9afbeb53d12e42230469fc5b26a7e14a01e51086f3f25c138f58a7002036fcc8f3ca054097b6dd7c71d639
+  languageName: node
+  linkType: hard
+
 "gopd@npm:^1.0.1":
   version: 1.0.1
   resolution: "gopd@npm:1.0.1"
@@ -4734,7 +4756,7 @@ __metadata:
   languageName: node
   linkType: hard
 
-"ignore@npm:^5.0.0, ignore@npm:^5.2.0, ignore@npm:^5.3.1":
+"ignore@npm:^5.0.0, ignore@npm:^5.2.0, ignore@npm:^5.2.4, ignore@npm:^5.3.1":
   version: 5.3.1
   resolution: "ignore@npm:5.3.1"
   checksum: 10/0a884c2fbc8c316f0b9f92beaf84464253b73230a4d4d286697be45fca081199191ca33e1c2e82d9e5f851f5e9a48a78e25a35c951e7eb41e59f150db3530065
@@ -7320,6 +7342,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"path-type@npm:^5.0.0":
+  version: 5.0.0
+  resolution: "path-type@npm:5.0.0"
+  checksum: 10/15ec24050e8932c2c98d085b72cfa0d6b4eeb4cbde151a0a05726d8afae85784fc5544f733d8dfc68536587d5143d29c0bd793623fad03d7e61cc00067291cd5
+  languageName: node
+  linkType: hard
+
 "pathe@npm:^1.1.0, pathe@npm:^1.1.1":
   version: 1.1.1
   resolution: "pathe@npm:1.1.1"
@@ -8306,6 +8335,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"slash@npm:^5.1.0":
+  version: 5.1.0
+  resolution: "slash@npm:5.1.0"
+  checksum: 10/2c41ec6fb1414cd9bba0fa6b1dd00e8be739e3fe85d079c69d4b09ca5f2f86eafd18d9ce611c0c0f686428638a36c272a6ac14799146a8295f259c10cc45cde4
+  languageName: node
+  linkType: hard
+
 "slice-ansi@npm:^5.0.0":
   version: 5.0.0
   resolution: "slice-ansi@npm:5.0.0"