Skip to content

Commit

Permalink
fix(dev): use Vite instead of esbuild, also Vitest (#416)
Browse files Browse the repository at this point in the history
This is _yet another_ change in how we generate features/templates. The
story have been to use esbuild, then ts-morph, back to esbuild and now
Vite. We use the Vite devServer directly to handle compiling the code so
we can import the template module to get the config off of it.

This change also replaces Jest with Vitest, which allows the testing of
esmodules much more easily. It was also required in order to actually
run Vite in some tests.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
mkilpatrick and github-actions[bot] authored Oct 19, 2023
1 parent dd0820b commit 67f29eb
Show file tree
Hide file tree
Showing 46 changed files with 572 additions and 2,160 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-extra-semi": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"import/extensions": ["error", "ignorePackages"]
"import/extensions": ["error", "ignorePackages"],
"react/no-deprecated": "off"
},
"settings": {
"react": {
Expand Down
2 changes: 1 addition & 1 deletion packages/pages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ This command will run the same build as the `docs:build` command and it will the
pnpm test
```

This command will run all the tests in the package using Jest as the test runner.
This command will run all the tests in the package using Vitest as the test runner.

## Commands to Use in a Starter

Expand Down
32 changes: 32 additions & 0 deletions packages/pages/THIRD-PARTY-NOTICES
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,38 @@ SOFTWARE.

-----------

The following npm package may be included in this product:

- [email protected]

This package contains the following license and notice below:

Copyright (c) 2015, Scott Motte
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

-----------

The following npm package may be included in this product:

- [email protected]
Expand Down
55 changes: 3 additions & 52 deletions packages/pages/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"lint": "eslint --cache --fix src/**",
"api-extractor": "api-extractor run --local --verbose",
"generate-docs": "api-documenter markdown --input-folder temp --output-folder docs/api && rm -rf temp",
"test": "jest",
"test": "vitest run --threads=false",
"generate-notices": "generate-license-file --input package.json --output ./THIRD-PARTY-NOTICES --overwrite",
"prepare": "pnpm build"
},
Expand All @@ -61,6 +61,7 @@
"cli-spinners": "^2.9.0",
"commander": "^11.0.0",
"cross-fetch": "^4.0.0",
"dotenv": "^16.3.1",
"escape-html": "^1.0.3",
"esm-module-paths": "^1.1.1",
"express": "^4.18.2",
Expand Down Expand Up @@ -95,7 +96,6 @@
"@types/express-serve-static-core": "^4.17.35",
"@types/fs-extra": "^11.0.1",
"@types/glob": "^8.1.0",
"@types/jest": "^29.5.3",
"@types/js-yaml": "4.0.5",
"@types/lodash": "^4.14.197",
"@types/mime-types": "^2.1.1",
Expand All @@ -110,62 +110,13 @@
"generate-license-file": "^2.0.0",
"glob": "^10.3.3",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"minimatch": "^9.0.3",
"ts-jest": "^29.1.1",
"ts-jest-mock-import-meta": "^1.1.0"
"vitest": "^0.34.6"
},
"peerDependencies": {
"react": "^17.0.2 || ^18.2.0",
"react-dom": "^17.0.2 || ^18.2.0"
},
"jest": {
"rootDir": "..",
"collectCoverageFrom": [
"<rootDir>/pages/src/**/*.{ts,tsx,js}"
],
"verbose": true,
"testMatch": [
"<rootDir>/pages/src/**/*.test.{ts,tsx,js}"
],
"transform": {
"\\.[jt]sx?$": [
"ts-jest",
{
"tsconfig": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"allowJs": true,
"target": "ESNext",
"jsx": "react"
},
"diagnostics": {
"ignoreCodes": [
1343
]
},
"astTransformers": {
"before": [
{
"path": "ts-jest-mock-import-meta",
"options": {
"metaObjectReplacement": {
"url": "JEST"
}
}
}
]
}
}
]
},
"moduleNameMapper": {
"^(.+)\\.js$": "$1",
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css)$": "identity-obj-proxy"
}
},
"overrides": {
"webpack": "^5.52.0"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import { determineAssetsFilepath } from "./getAssetsFilepath.js";
import * as importHelper from "./import.js";

Expand Down Expand Up @@ -28,7 +29,7 @@ describe("getAssetsFilepath - determineAssetsFilepath", () => {
},
};

const importSpy = jest.spyOn(importHelper, "import_");
const importSpy = vi.spyOn(importHelper, "import_");
importSpy.mockImplementation(async () => viteConfig);

const actual = await determineAssetsFilepath(
Expand Down
1 change: 1 addition & 0 deletions packages/pages/src/common/src/feature/features.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect } from "vitest";
import {
convertTemplateConfigToFeatureConfig,
convertTemplateConfigInternalToFeaturesConfig,
Expand Down
1 change: 1 addition & 0 deletions packages/pages/src/common/src/feature/stream.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect } from "vitest";
import { convertTemplateConfigToStreamConfig, StreamConfig } from "./stream.js";
import { TemplateConfigInternal } from "../template/internal/types.js";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import path from "path";
import { getFunctionFilepaths } from "./getFunctionFilepaths.js";
import { minimatch } from "minimatch";
Expand Down Expand Up @@ -43,13 +44,11 @@ const expected = [
},
];

jest.mock("glob", () => {
return {
globSync: (glob: string) => {
return filepaths.filter((f) => minimatch(path.resolve(f), glob));
},
};
});
vi.mock("glob", () => ({
globSync: (glob: string) => {
return filepaths.filter((f) => minimatch(path.resolve(f), glob));
},
}));

describe("getFunctionFilepaths", () => {
it("collects all function files under the src/functions path", () => {
Expand Down
20 changes: 1 addition & 19 deletions packages/pages/src/common/src/function/internal/loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,8 @@
import { describe, it, expect } from "vitest";
import path from "path";
import { loadFunctionModules, FunctionModuleCollection } from "./loader.js";
import { ProjectStructure } from "../../project/structure.js";

// our jest configuration doesn't support file urls so update pathToFileURL to do nothing during
// this test.
jest.mock("url", () => {
const original = jest.requireActual("url");
return {
__esModule: true,
...original,
pathToFileURL: (s: string) => s,
};
});

jest.mock("vite", () => {
return {
loadEnv: () => [],
};
});

afterAll(() => jest.unmock("url"));

describe("loadTemplateModules", () => {
const projectStructure = new ProjectStructure();

Expand Down
8 changes: 6 additions & 2 deletions packages/pages/src/common/src/function/internal/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "./types.js";
import { getFunctionFilepaths } from "./getFunctionFilepaths.js";
import { ProjectStructure } from "../../project/structure.js";
import { loadModules } from "../../loader/esbuild.js";
import { loadModules } from "../../loader/vite.js";

/**
* Loads all functions in the project.
Expand All @@ -23,7 +23,11 @@ export const loadFunctionModules = async (
path.format(functionPath)
);

const importedModules = await loadModules(functionPathStrings, transpile);
const importedModules = await loadModules(
functionPathStrings,
transpile,
projectStructure
);

const importedFunctionModules = [] as FunctionModuleInternal[];
for (const importedModule of importedModules) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect } from "vitest";
import path from "path";
import { convertFunctionModuleToFunctionModuleInternal } from "./types.js";
import {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect } from "vitest";
import { HttpFunction } from "../types.js";
import { validateFunctionModule } from "./validateFunctionModule.js";

Expand Down
70 changes: 70 additions & 0 deletions packages/pages/src/common/src/loader/vite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { InlineConfig, createServer } from "vite";
import { ProjectStructure } from "../project/structure.js";
import { processEnvVariables } from "../../../util/processEnvVariables.js";
import { pathToFileURL } from "node:url";
import { loadViteModule } from "../../../dev/server/ssr/loadViteModule.js";

export const getViteServerConfig = (
projectStructure: ProjectStructure
): InlineConfig => {
return {
server: {
middlewareMode: true,
},
appType: "custom",
envDir: projectStructure.config.envVarConfig.envVarDir,
envPrefix: projectStructure.config.envVarConfig.envVarPrefix,
define: processEnvVariables(
projectStructure.config.envVarConfig.envVarPrefix
),
optimizeDeps: {
include: ["react-dom", "react-dom/client"],
},
};
};

export type ImportedModule = {
path: string;
module: any;
};

/**
* Loads any files as modules.
*
* @param modulePaths the filepaths to load as modules
* @param transpile set to true if the paths need to be transpiled (such as when they are in tsx format)
* @returns Promise<{@link ImportedModule[]}>
*/
export const loadModules = async (
modulePaths: string[],
transpile: boolean,
projectStructure: ProjectStructure
): Promise<ImportedModule[]> => {
const importedModules: ImportedModule[] = [];

if (transpile) {
// Note that this will log inconsequential errors
// See https://github.com/vitejs/vite/issues/14328
const vite = await createServer(getViteServerConfig(projectStructure));

for (const modulePath of modulePaths) {
const functionModule = await loadViteModule(vite, modulePath);

importedModules.push({
path: modulePath,
module: functionModule,
});
}

await vite.close();
} else {
for (const modulePath of modulePaths) {
importedModules.push({
path: modulePath,
module: await import(pathToFileURL(modulePath).toString()),
});
}
}

return importedModules;
};
14 changes: 6 additions & 8 deletions packages/pages/src/common/src/project/structure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,14 +192,12 @@ export class ProjectStructure {
) => {
const config = merge(defaultProjectStructureConfig, projectStructureConfig);

const assetsDir =
process.env.JEST_WORKER_ID !== undefined // vite.config.js cannot be resolved during tests
? DEFAULT_ASSETS_DIR
: await determineAssetsFilepath(
DEFAULT_ASSETS_DIR,
pathLib.resolve(config.rootFiles.config),
pathLib.resolve("vite.config.js")
); // TODO: handle other extensions
// TODO: handle other extensions
const assetsDir = await determineAssetsFilepath(
DEFAULT_ASSETS_DIR,
pathLib.resolve(config.rootFiles.config),
pathLib.resolve("vite.config.js")
);

config.subfolders.assets = assetsDir;

Expand Down
7 changes: 4 additions & 3 deletions packages/pages/src/common/src/template/head.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import { HeadConfig, renderHeadConfigToString, getLang } from "./head.js";
import { TemplateRenderProps } from "./types.js";
import pc from "picocolors";
Expand Down Expand Up @@ -150,8 +151,8 @@ describe("renderHeadConfigToString", () => {
`Please use "other" to render this tag.`
);

jest.clearAllMocks();
const logMock = jest
vi.clearAllMocks();
const logMock = vi
.spyOn(console, "log")
.mockImplementation(() => undefined);

Expand All @@ -160,7 +161,7 @@ describe("renderHeadConfigToString", () => {
expect(logMock.mock.calls.length).toBe(1);
expect(logMock.mock.calls[0][0]).toBe(expectedLog);

jest.clearAllMocks();
vi.clearAllMocks();
});
});

Expand Down
Loading

0 comments on commit 67f29eb

Please sign in to comment.