зеркало из https://github.com/microsoft/rnx-kit.git
TypeScript validation during Metro bundling (#195)
* Create a typescript validation plugin for Metro. Add the plugin to our test-app package. Update test-app source files to typescript, and change the Metro entry file from lib (transpiled) to src. Separately, split tsconfig into options and file-specs. Update all packages to import the options-only shared tsconfig, so they can specify their own source files. * Add a VSCode launch profile for attaching to running node process. * Update dependencies * Change files * Fix string handling bug * Move code into its own source file, exporting everything for testability. * Add tests and a README file * Split tsconfig options out into a shared file that can be included in all projects without specifying any source files. * Change files * Switch to shared babel config * Wipe out change files * Change files * Remove unnecessary yargs churn in yarn.lock * fix path bugs on windows * Fix another windows/posix path bug. Add more tests to cover the missed case. * Updates based on PR feedback
This commit is contained in:
Родитель
b43bad128a
Коммит
368a92fbae
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Attach by Process ID",
|
||||
"processId": "${command:PickProcess}",
|
||||
"request": "attach",
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"type": "pwa-node"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "patch",
|
||||
"comment": "Create a typescript validation plugin for Metro. Add the plugin to our test-app package. Update test-app source files to typescript, and change the Metro entry file from lib (transpiled) to src.",
|
||||
"packageName": "@rnx-kit/metro-plugin-typescript-validation",
|
||||
"email": "afoxman@microsoft.com",
|
||||
"dependentChangeType": "patch"
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
# @rnx-kit/metro-plugin-typescript-validation
|
||||
|
||||
`@rnx-kit/metro-plugin-typescript-validation` checks TypeScript source files in your package for syntactic and semantic correctness.
|
||||
|
||||
## Usage
|
||||
|
||||
Add this plugin in your `metro.config.js` using `@rnx-kit/metro-serializer`:
|
||||
|
||||
```js
|
||||
const { makeMetroConfig } = require("@rnx-kit/metro-config");
|
||||
const {
|
||||
TypeScriptValidation,
|
||||
} = require("@rnx-kit/metro-plugin-typescript-validation");
|
||||
const { MetroSerializer } = require("@rnx-kit/metro-serializer");
|
||||
|
||||
module.exports = makeMetroConfig({
|
||||
projectRoot: __dirname,
|
||||
serializer: {
|
||||
customSerializer: MetroSerializer([TypeScriptValidation()]),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
This plugin runs as part of Metro bundling. When a type error occurs, it is displayed console output and bundle creation fails (no files are written).
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
presets: ["@rnx-kit/babel-preset-jest-typescript"],
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
const { configureJust } = require("rnx-kit-scripts");
|
||||
configureJust();
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"name": "@rnx-kit/metro-plugin-typescript-validation",
|
||||
"version": "1.0.0",
|
||||
"description": "Typescript validation during Metro bundling",
|
||||
"homepage": "https://github.com/microsoft/rnx-kit/tree/main/packages/metro-plugin-typescript-validation#rnx-kitmetro-plugin-typescript-validation",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"lib/*"
|
||||
],
|
||||
"main": "lib/index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/microsoft/rnx-kit",
|
||||
"directory": "packages/metro-plugin-typescript-validation"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rnx-kit-scripts build",
|
||||
"test": "rnx-kit-scripts test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@msfast/typescript-platform-resolution": "^4.2.4-midgard.0",
|
||||
"yargs": "^16.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/yargs": "^16.0.0",
|
||||
"@rnx-kit/babel-preset-jest-typescript": "*",
|
||||
"@rnx-kit/metro-serializer": "*",
|
||||
"rnx-kit-scripts": "*"
|
||||
},
|
||||
"jest": {
|
||||
"roots": [
|
||||
"test"
|
||||
],
|
||||
"testRegex": "/test/.*\\.test\\.ts$"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { TypeScriptValidation } from "./plugin";
|
|
@ -0,0 +1,139 @@
|
|||
import type {
|
||||
Graph,
|
||||
MetroPlugin,
|
||||
Module,
|
||||
SerializerOptions,
|
||||
} from "@rnx-kit/metro-serializer";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as child_process from "child_process";
|
||||
const yargs = require("yargs/yargs");
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
export function getModuleRoot(module: string): string {
|
||||
return path.dirname(require.resolve(module + "/package.json"));
|
||||
}
|
||||
|
||||
export function readTsConfig(projectRoot: string) {
|
||||
const p = path.join(projectRoot, "tsconfig.json");
|
||||
return JSON.parse(fs.readFileSync(p, { encoding: "utf8" }));
|
||||
}
|
||||
|
||||
export function writeMetroTsConfig(
|
||||
projectRoot: string,
|
||||
tsconfig: object
|
||||
): string {
|
||||
const json = JSON.stringify(tsconfig);
|
||||
const tsconfigMetroPath = path.join(
|
||||
projectRoot,
|
||||
"node_modules",
|
||||
"tsconfig-metro.json"
|
||||
);
|
||||
fs.writeFileSync(tsconfigMetroPath, json);
|
||||
return tsconfigMetroPath;
|
||||
}
|
||||
|
||||
export function runTypeScriptCompiler(projectPath: string) {
|
||||
const tscPath = path.join(
|
||||
getModuleRoot("@msfast/typescript-platform-resolution"),
|
||||
"lib",
|
||||
"tsc.js"
|
||||
);
|
||||
|
||||
const spawnOptions: child_process.SpawnSyncOptions = {
|
||||
cwd: process.cwd(),
|
||||
stdio: "inherit",
|
||||
};
|
||||
const args = [tscPath, "--project", projectPath];
|
||||
const processInfo = child_process.spawnSync(
|
||||
process.execPath,
|
||||
args,
|
||||
spawnOptions
|
||||
);
|
||||
if (processInfo.status) {
|
||||
throw (
|
||||
processInfo.error ||
|
||||
new Error(
|
||||
"TypeScript validation failed with exit code " + processInfo.status
|
||||
)
|
||||
);
|
||||
} else if (processInfo.signal) {
|
||||
throw (
|
||||
processInfo.error ||
|
||||
new Error("TypeScript validation crashed due to " + processInfo.signal)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function visit(
|
||||
modulePath: string,
|
||||
graph: Graph,
|
||||
scopePath: string,
|
||||
visited: Record<string, boolean>,
|
||||
files: Array<string>
|
||||
) {
|
||||
// avoid circular references in the dependency graph
|
||||
if (modulePath in visited) {
|
||||
return;
|
||||
}
|
||||
visited[modulePath] = true;
|
||||
|
||||
// collect any file that is in scope
|
||||
if (modulePath.startsWith(scopePath)) {
|
||||
files.push(modulePath);
|
||||
}
|
||||
|
||||
// recursively visit children
|
||||
graph.dependencies
|
||||
.get(modulePath)
|
||||
?.dependencies?.forEach((m) =>
|
||||
visit(m.absolutePath, graph, scopePath, visited, files)
|
||||
);
|
||||
}
|
||||
|
||||
export function TypeScriptValidation(): MetroPlugin {
|
||||
// read the --platform argument from the Metro command-line
|
||||
const argv = yargs(hideBin(process.argv)).argv;
|
||||
const platform = argv.platform.toLowerCase();
|
||||
const resolutionPlatforms = ["win32", "windows"].includes(platform)
|
||||
? [platform, "win", "native"]
|
||||
: [platform, "native"];
|
||||
|
||||
return (
|
||||
_entryPoint: string,
|
||||
_preModules: ReadonlyArray<Module>,
|
||||
graph: Graph,
|
||||
options: SerializerOptions
|
||||
) => {
|
||||
const visited: Record<string, boolean> = {};
|
||||
const files: Array<string> = [];
|
||||
|
||||
graph.entryPoints.forEach((m) =>
|
||||
visit(m, graph, options.projectRoot, visited, files)
|
||||
);
|
||||
|
||||
const tsconfig = readTsConfig(options.projectRoot);
|
||||
|
||||
// remove include/exclude directives
|
||||
delete tsconfig.include;
|
||||
delete tsconfig.exclude;
|
||||
|
||||
// set the specific list of files to type-check
|
||||
tsconfig.files = files;
|
||||
|
||||
// compiler options:
|
||||
// - don't emit any codegen files
|
||||
// - resolve modules using platform overrides
|
||||
tsconfig.compilerOptions = tsconfig.compilerOptions || {};
|
||||
tsconfig.compilerOptions.noEmit = true;
|
||||
tsconfig.compilerOptions.resolutionPlatforms = resolutionPlatforms;
|
||||
|
||||
// write the altered tsconfig, run TSC, and then cleanup the altered tsconfig
|
||||
const tsconfigMetroPath = writeMetroTsConfig(options.projectRoot, tsconfig);
|
||||
try {
|
||||
runTypeScriptCompiler(tsconfigMetroPath);
|
||||
} finally {
|
||||
fs.unlinkSync(tsconfigMetroPath);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`readTsConfig() reads the contents of tsconfig.json 1`] = `
|
||||
Object {
|
||||
"compilerOptions": Object {
|
||||
"noEmit": true,
|
||||
},
|
||||
"include": Array [
|
||||
"src",
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`runTypeScriptCompiler() throws when the process crashes 1`] = `"TypeScript validation crashed due to SIGKILL"`;
|
||||
|
||||
exports[`runTypeScriptCompiler() throws when the process exit code is non-zero 1`] = `"TypeScript validation failed with exit code 1"`;
|
||||
|
||||
exports[`runTypeScriptCompiler() throws when the process exit code is non-zero and an error is given 1`] = `"simulated error message from typescript compiler"`;
|
||||
|
||||
exports[`runTypeScriptCompiler() throws when the process exit code is non-zero and an error is given 2`] = `"simulated crash message caused by sending SIGKILL to the typescript compiler"`;
|
||||
|
||||
exports[`visit() returns a list of files in the current package (posix) 1`] = `
|
||||
Array [
|
||||
"/repos/yoyodyne/packages/overthruster/src/main.ts",
|
||||
"/repos/yoyodyne/packages/overthruster/src/propulsion.ts",
|
||||
"/repos/yoyodyne/packages/overthruster/src/dimensions.ts",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`visit() returns a list of files in the current package (win32) 1`] = `
|
||||
Array [
|
||||
"C:\\\\repos\\\\yoyodyne\\\\packages\\\\overthruster\\\\src\\\\main.ts",
|
||||
"C:\\\\repos\\\\yoyodyne\\\\packages\\\\overthruster\\\\src\\\\propulsion.ts",
|
||||
"C:\\\\repos\\\\yoyodyne\\\\packages\\\\overthruster\\\\src\\\\dimensions.ts",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`visit() traverses the entire graph (posix) 1`] = `
|
||||
Object {
|
||||
"/repos/yoyodyne/node_modules/react-native/index.js": true,
|
||||
"/repos/yoyodyne/packages/overthruster/src/dimensions.ts": true,
|
||||
"/repos/yoyodyne/packages/overthruster/src/main.ts": true,
|
||||
"/repos/yoyodyne/packages/overthruster/src/propulsion.ts": true,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`visit() traverses the entire graph (win32) 1`] = `
|
||||
Object {
|
||||
"/repos/yoyodyne/packages/overthruster\\\\src\\\\propulsion.ts": true,
|
||||
"C:\\\\repos\\\\yoyodyne\\\\node_modules\\\\react-native\\\\index.js": true,
|
||||
"C:\\\\repos\\\\yoyodyne\\\\packages\\\\overthruster\\\\src\\\\dimensions.ts": true,
|
||||
"C:\\\\repos\\\\yoyodyne\\\\packages\\\\overthruster\\\\src\\\\main.ts": true,
|
||||
"C:\\\\repos\\\\yoyodyne\\\\packages\\\\overthruster\\\\src\\\\propulsion.ts": true,
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,456 @@
|
|||
import type {
|
||||
Dependency,
|
||||
Graph,
|
||||
Module,
|
||||
MixedOutput,
|
||||
SerializerOptions,
|
||||
} from "@rnx-kit/metro-serializer";
|
||||
import {
|
||||
getModuleRoot,
|
||||
readTsConfig,
|
||||
writeMetroTsConfig,
|
||||
runTypeScriptCompiler,
|
||||
visit,
|
||||
TypeScriptValidation,
|
||||
} from "../src/plugin";
|
||||
import path from "path";
|
||||
const child_process = require("child_process");
|
||||
const fs = require("fs");
|
||||
const yargs = require("yargs/yargs");
|
||||
import { hideBin } from "yargs/helpers";
|
||||
|
||||
jest.mock("child_process");
|
||||
jest.mock("fs");
|
||||
jest.mock("yargs/yargs");
|
||||
jest.mock("yargs/helpers");
|
||||
|
||||
// test data
|
||||
|
||||
const rootPathPosix = "/repos/yoyodyne/packages/overthruster";
|
||||
const rootPathWin32 = path.win32.normalize("C:" + rootPathPosix);
|
||||
|
||||
const graphPosix: Graph = {
|
||||
dependencies: new Map<string, Module<MixedOutput>>(),
|
||||
importBundleNames: new Set(),
|
||||
entryPoints: [rootPathPosix + "/src/main.ts"],
|
||||
};
|
||||
const graphWin32: Graph = {
|
||||
dependencies: new Map<string, Module<MixedOutput>>(),
|
||||
importBundleNames: new Set(),
|
||||
entryPoints: [rootPathWin32 + "\\src\\main.ts"],
|
||||
};
|
||||
|
||||
function addGraphDependency(g: Graph, modulePath: string) {
|
||||
g.dependencies.set(modulePath, {
|
||||
dependencies: new Map<string, Dependency>(),
|
||||
inverseDependencies: new Set<string>(),
|
||||
output: [],
|
||||
path: modulePath,
|
||||
getSource: () => {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
return g.dependencies.get(modulePath);
|
||||
}
|
||||
|
||||
function addModuleDependency(
|
||||
g: Graph,
|
||||
parent,
|
||||
moduleName: string,
|
||||
modulePath: string
|
||||
) {
|
||||
parent.dependencies.set(moduleName, {
|
||||
absolutePath: modulePath,
|
||||
});
|
||||
|
||||
return addGraphDependency(g, modulePath);
|
||||
}
|
||||
|
||||
const index_ts_posix = addGraphDependency(
|
||||
graphPosix,
|
||||
rootPathPosix + "/src/main.ts"
|
||||
);
|
||||
const index_ts_win32 = addGraphDependency(
|
||||
graphWin32,
|
||||
rootPathWin32 + "\\src\\main.ts"
|
||||
);
|
||||
|
||||
const propulsion_ts_posix = addModuleDependency(
|
||||
graphPosix,
|
||||
index_ts_posix,
|
||||
"./src/propulsion.ts",
|
||||
rootPathPosix + "/src/propulsion.ts"
|
||||
);
|
||||
const propulsion_ts_win32 = addModuleDependency(
|
||||
graphWin32,
|
||||
index_ts_win32,
|
||||
".\\src\\propulsion.ts",
|
||||
rootPathWin32 + "\\src\\propulsion.ts"
|
||||
);
|
||||
|
||||
const dimension_ts_posix = addModuleDependency(
|
||||
graphPosix,
|
||||
index_ts_posix,
|
||||
"./src/dimensions.ts",
|
||||
rootPathPosix + "/src/dimensions.ts"
|
||||
);
|
||||
const dimension_ts_win32 = addModuleDependency(
|
||||
graphWin32,
|
||||
index_ts_win32,
|
||||
".\\src\\dimensions.ts",
|
||||
rootPathWin32 + "\\src\\dimensions.ts"
|
||||
);
|
||||
|
||||
const react_native_posix = addModuleDependency(
|
||||
graphPosix,
|
||||
index_ts_posix,
|
||||
"react-native",
|
||||
"/repos/yoyodyne/node_modules/react-native/index.js"
|
||||
);
|
||||
const react_native_win32 = addModuleDependency(
|
||||
graphWin32,
|
||||
index_ts_win32,
|
||||
"react-native",
|
||||
"C:\\repos\\yoyodyne\\node_modules\\react-native\\index.js"
|
||||
);
|
||||
|
||||
// create a circular dependency
|
||||
propulsion_ts_posix.dependencies.set("./src/dimensions.ts", {
|
||||
absolutePath: rootPathPosix + "/src/dimensions.ts",
|
||||
data: null,
|
||||
});
|
||||
propulsion_ts_win32.dependencies.set(".\\src\\dimensions.ts", {
|
||||
absolutePath: rootPathWin32 + "\\src\\dimensions.ts",
|
||||
data: null,
|
||||
});
|
||||
|
||||
dimension_ts_posix.dependencies.set("./src/propulsion.ts", {
|
||||
absolutePath: rootPathPosix + "/src/propulsion.ts",
|
||||
data: null,
|
||||
});
|
||||
dimension_ts_win32.dependencies.set(".\\src\\propulsion.ts", {
|
||||
absolutePath: rootPathPosix + "\\src\\propulsion.ts",
|
||||
data: null,
|
||||
});
|
||||
|
||||
// test suite
|
||||
|
||||
describe("getModuleRoot()", () => {
|
||||
test("throws on unknown module", () => {
|
||||
expect(() => {
|
||||
getModuleRoot("not-a-real-module");
|
||||
}).toThrowError();
|
||||
});
|
||||
|
||||
test("resolves to the root of @msfast/typescript-platform-resolution", () => {
|
||||
expect(getModuleRoot("@msfast/typescript-platform-resolution")).toEqual(
|
||||
expect.stringMatching(/@msfast[/\\]typescript-platform-resolution$/)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("readTsConfig()", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test("reads the contents of tsconfig.json", () => {
|
||||
fs.readFileSync.mockReturnValue(
|
||||
JSON.stringify({
|
||||
compilerOptions: { noEmit: true },
|
||||
include: ["src"],
|
||||
})
|
||||
);
|
||||
|
||||
const p = path.normalize("/path/to/file");
|
||||
expect(readTsConfig(p)).toMatchSnapshot();
|
||||
expect(fs.readFileSync).toBeCalledTimes(1);
|
||||
expect(fs.readFileSync).toBeCalledWith(
|
||||
path.join(p, "tsconfig.json"),
|
||||
expect.anything()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("writeMetroTsConfig()", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test("writes the contents of tsconfig to a temp file in node_modules", () => {
|
||||
const projectRoot = "/root/path/here";
|
||||
const tsconfig = { a: 123 };
|
||||
|
||||
writeMetroTsConfig(projectRoot, tsconfig);
|
||||
|
||||
expect(fs.writeFileSync).toBeCalledTimes(1);
|
||||
expect(fs.writeFileSync).toBeCalledWith(
|
||||
expect.stringMatching(/[/\\]node_modules[/\\]tsconfig-metro.json$/),
|
||||
'{"a":123}'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("runTypeScriptCompiler()", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test("executes the typescript compiler using node.js", () => {
|
||||
child_process.spawnSync.mockReturnValue({});
|
||||
|
||||
runTypeScriptCompiler("/path/to/project/tsconfig.json");
|
||||
|
||||
expect(child_process.spawnSync).toBeCalledTimes(1);
|
||||
const spawnSyncParams = child_process.spawnSync.mock.calls[0];
|
||||
const { [0]: executable, [1]: args } = spawnSyncParams;
|
||||
expect(executable).toEqual(process.execPath);
|
||||
expect(args[0]).toEqual(
|
||||
expect.stringMatching(
|
||||
/[/\\]@msfast[/\\]typescript-platform-resolution.*[/\\]tsc.js$/
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test("succeeds when the process exit code is zero", () => {
|
||||
child_process.spawnSync.mockReturnValue({
|
||||
status: 0,
|
||||
});
|
||||
|
||||
runTypeScriptCompiler("/path/to/project/tsconfig.json");
|
||||
expect(child_process.spawnSync).toBeCalledTimes(1);
|
||||
});
|
||||
|
||||
test("throws when the process exit code is non-zero", () => {
|
||||
child_process.spawnSync.mockReturnValue({
|
||||
status: 1,
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
runTypeScriptCompiler("/path/to/project/tsconfig.json")
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test("throws when the process exit code is non-zero and an error is given", () => {
|
||||
child_process.spawnSync.mockReturnValue({
|
||||
status: 1,
|
||||
error: new Error("simulated error message from typescript compiler"),
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
runTypeScriptCompiler("/path/to/project/tsconfig.json")
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test("throws when the process crashes", () => {
|
||||
child_process.spawnSync.mockReturnValue({
|
||||
signal: "SIGKILL",
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
runTypeScriptCompiler("/path/to/project/tsconfig.json")
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
test("throws when the process exit code is non-zero and an error is given", () => {
|
||||
child_process.spawnSync.mockReturnValue({
|
||||
signal: "SIGKILL",
|
||||
error: new Error(
|
||||
"simulated crash message caused by sending SIGKILL to the typescript compiler"
|
||||
),
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
runTypeScriptCompiler("/path/to/project/tsconfig.json")
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("visit()", () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test("traverses the entire graph (posix)", () => {
|
||||
const visited: Record<string, boolean> = {};
|
||||
const files: Array<string> = [];
|
||||
|
||||
graphPosix.entryPoints.forEach((m) =>
|
||||
visit(m, graphPosix, rootPathPosix, visited, files)
|
||||
);
|
||||
expect(visited).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("traverses the entire graph (win32)", () => {
|
||||
const visited: Record<string, boolean> = {};
|
||||
const files: Array<string> = [];
|
||||
|
||||
graphWin32.entryPoints.forEach((m) =>
|
||||
visit(m, graphWin32, rootPathWin32, visited, files)
|
||||
);
|
||||
expect(visited).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("returns a list of files in the current package (posix)", () => {
|
||||
const visited: Record<string, boolean> = {};
|
||||
const files: Array<string> = [];
|
||||
|
||||
graphPosix.entryPoints.forEach((m) =>
|
||||
visit(m, graphPosix, rootPathPosix, visited, files)
|
||||
);
|
||||
expect(files).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("returns a list of files in the current package (win32)", () => {
|
||||
const visited: Record<string, boolean> = {};
|
||||
const files: Array<string> = [];
|
||||
|
||||
graphWin32.entryPoints.forEach((m) =>
|
||||
visit(m, graphWin32, rootPathWin32, visited, files)
|
||||
);
|
||||
expect(files).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("TypeScriptValidation()", () => {
|
||||
beforeEach(() => {
|
||||
yargs.mockReturnValue({
|
||||
argv: {
|
||||
platform: "test-platform",
|
||||
},
|
||||
});
|
||||
|
||||
fs.readFileSync.mockReturnValue(
|
||||
JSON.stringify({
|
||||
include: ["src"],
|
||||
})
|
||||
);
|
||||
|
||||
child_process.spawnSync.mockReturnValue({
|
||||
status: 0,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test("replaces file specification with list from graph", () => {
|
||||
TypeScriptValidation()(undefined, undefined, graphPosix, {
|
||||
projectRoot: rootPathPosix,
|
||||
} as SerializerOptions);
|
||||
|
||||
expect(fs.writeFileSync).toBeCalledTimes(1);
|
||||
const tsconfig = JSON.parse(fs.writeFileSync.mock.calls[0][1]);
|
||||
expect(tsconfig?.include).toBeFalsy();
|
||||
expect(tsconfig?.exclude).toBeFalsy();
|
||||
expect(tsconfig?.files).toBeTruthy();
|
||||
expect(Array.isArray(tsconfig.files)).toBeTruthy();
|
||||
expect(tsconfig.files.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("adds noEmit compiler option", () => {
|
||||
TypeScriptValidation()(undefined, undefined, graphPosix, {
|
||||
projectRoot: rootPathPosix,
|
||||
} as SerializerOptions);
|
||||
|
||||
expect(fs.writeFileSync).toBeCalledTimes(1);
|
||||
const tsconfig = JSON.parse(fs.writeFileSync.mock.calls[0][1]);
|
||||
expect(tsconfig?.compilerOptions?.noEmit).toBeTruthy();
|
||||
});
|
||||
|
||||
function testResolutionPlatforms(
|
||||
platform: string,
|
||||
expectedPlatforms: string[]
|
||||
) {
|
||||
yargs.mockReturnValue({
|
||||
argv: {
|
||||
platform,
|
||||
},
|
||||
});
|
||||
|
||||
TypeScriptValidation()(undefined, undefined, graphPosix, {
|
||||
projectRoot: rootPathPosix,
|
||||
} as SerializerOptions);
|
||||
|
||||
expect(fs.writeFileSync).toBeCalledTimes(1);
|
||||
const tsconfig = JSON.parse(fs.writeFileSync.mock.calls[0][1]);
|
||||
expect(tsconfig?.compilerOptions?.resolutionPlatforms).toEqual(
|
||||
expectedPlatforms
|
||||
);
|
||||
}
|
||||
|
||||
test("adds list of resolution platforms (android)", () =>
|
||||
testResolutionPlatforms("android", ["android", "native"]));
|
||||
|
||||
test("adds list of resolution platforms (ios)", () =>
|
||||
testResolutionPlatforms("ios", ["ios", "native"]));
|
||||
|
||||
test("adds list of resolution platforms (macos)", () =>
|
||||
testResolutionPlatforms("macos", ["macos", "native"]));
|
||||
|
||||
test("adds list of resolution platforms (windows)", () =>
|
||||
testResolutionPlatforms("windows", ["windows", "win", "native"]));
|
||||
|
||||
test("adds list of resolution platforms (win32)", () =>
|
||||
testResolutionPlatforms("win32", ["win32", "win", "native"]));
|
||||
|
||||
test("adds list of resolution platforms (win)", () =>
|
||||
testResolutionPlatforms("win", ["win", "native"]));
|
||||
|
||||
test("adds list of resolution platforms (FaKe-PLAtfORm)", () =>
|
||||
testResolutionPlatforms("FaKe-PLAtfORm", ["fake-platform", "native"]));
|
||||
|
||||
test("runs typescript compiler", () => {
|
||||
TypeScriptValidation()(undefined, undefined, graphPosix, {
|
||||
projectRoot: rootPathPosix,
|
||||
} as SerializerOptions);
|
||||
|
||||
expect(child_process.spawnSync).toBeCalledTimes(1);
|
||||
const spawnSyncParams = child_process.spawnSync.mock.calls[0];
|
||||
const { [1]: args } = spawnSyncParams;
|
||||
expect(args[0]).toEqual(
|
||||
expect.stringMatching(
|
||||
/[/\\]@msfast[/\\]typescript-platform-resolution.*[/\\]tsc.js$/
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test("runs typescript compiler", () => {
|
||||
TypeScriptValidation()(undefined, undefined, graphPosix, {
|
||||
projectRoot: rootPathPosix,
|
||||
} as SerializerOptions);
|
||||
|
||||
expect(child_process.spawnSync).toBeCalledTimes(1);
|
||||
const spawnSyncParams = child_process.spawnSync.mock.calls[0];
|
||||
const { [1]: args } = spawnSyncParams;
|
||||
expect(args[0]).toEqual(
|
||||
expect.stringMatching(
|
||||
/[/\\]@msfast[/\\]typescript-platform-resolution.*[/\\]tsc.js$/
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test("cleans up temporary tsconfig file when typescript compiler succeeds", () => {
|
||||
TypeScriptValidation()(undefined, undefined, graphPosix, {
|
||||
projectRoot: rootPathPosix,
|
||||
} as SerializerOptions);
|
||||
|
||||
expect(fs.unlinkSync).toBeCalledTimes(1);
|
||||
expect(fs.unlinkSync).toBeCalledWith(
|
||||
expect.stringMatching(/[/\\]node_modules[/\\]tsconfig-metro.json$/)
|
||||
);
|
||||
});
|
||||
|
||||
test("cleans up temporary tsconfig file when typescript compiler fails", () => {
|
||||
child_process.spawnSync.mockReturnValue({
|
||||
status: 1,
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
TypeScriptValidation()(undefined, undefined, graphPosix, {
|
||||
projectRoot: rootPathPosix,
|
||||
} as SerializerOptions);
|
||||
}).toThrowError();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": "rnx-kit-scripts/tsconfig-shared.json",
|
||||
"include": ["src"]
|
||||
}
|
|
@ -6,6 +6,9 @@ const {
|
|||
DuplicateDependencies,
|
||||
} = require("@rnx-kit/metro-plugin-duplicates-checker");
|
||||
const { MetroSerializer } = require("@rnx-kit/metro-serializer");
|
||||
const {
|
||||
TypeScriptValidation,
|
||||
} = require("@rnx-kit/metro-plugin-typescript-validation");
|
||||
|
||||
module.exports = makeMetroConfig({
|
||||
projectRoot: __dirname,
|
||||
|
@ -13,6 +16,7 @@ module.exports = makeMetroConfig({
|
|||
customSerializer: MetroSerializer([
|
||||
CyclicDependencies(),
|
||||
DuplicateDependencies(),
|
||||
TypeScriptValidation(),
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"@rnx-kit/metro-config": "*",
|
||||
"@rnx-kit/metro-plugin-cyclic-dependencies-detector": "*",
|
||||
"@rnx-kit/metro-plugin-duplicates-checker": "*",
|
||||
"@rnx-kit/metro-plugin-typescript-validation": "*",
|
||||
"@rnx-kit/metro-serializer": "*",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-native": "^0.63.50",
|
||||
|
@ -50,7 +51,7 @@
|
|||
"reactNativeVersion": "^0.63",
|
||||
"kitType": "app",
|
||||
"bundle": {
|
||||
"entryPath": "lib/src/index.js",
|
||||
"entryPath": "src/index.ts",
|
||||
"distPath": "dist",
|
||||
"assetsPath": "dist",
|
||||
"bundlePrefix": "main",
|
||||
|
|
|
@ -1442,6 +1442,11 @@
|
|||
memory-streams "^0.1.3"
|
||||
p-graph "^1.1.0"
|
||||
|
||||
"@msfast/typescript-platform-resolution@^4.2.4-midgard.0":
|
||||
version "4.2.4-midgard.0"
|
||||
resolved "https://registry.yarnpkg.com/@msfast/typescript-platform-resolution/-/typescript-platform-resolution-4.2.4-midgard.0.tgz#c29f4fbc7d7e772517f2eb207603831de6bfcff1"
|
||||
integrity sha512-o0jiFEWlTVkvu3UbbF9rLel8sf1mMZBLpRrJl6wM3odYzXNZwbPXPGd6Nj3IYYAd+laEUkm1gqurDUJJWaiskQ==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.4":
|
||||
version "2.1.4"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz#d4b3549a5db5de2683e0c1071ab4f140904bbf69"
|
||||
|
|
Загрузка…
Ссылка в новой задаче