Merge pull request #30402 from Microsoft/resolveJsonModule
Handle json files included in the project from project reference redirect
This commit is contained in:
commit
81ed511de2
17 changed files with 183 additions and 41 deletions
|
@ -980,7 +980,7 @@ namespace ts {
|
|||
backupState: noop,
|
||||
restoreState: noop,
|
||||
getProgram: notImplemented,
|
||||
getProgramOrUndefined: () => undefined,
|
||||
getProgramOrUndefined: returnUndefined,
|
||||
releaseProgram: noop,
|
||||
getCompilerOptions: () => state.compilerOptions,
|
||||
getSourceFile: notImplemented,
|
||||
|
|
|
@ -1614,6 +1614,9 @@ namespace ts {
|
|||
/** Do nothing and return true */
|
||||
export function returnTrue(): true { return true; }
|
||||
|
||||
/** Do nothing and return undefined */
|
||||
export function returnUndefined(): undefined { return undefined; }
|
||||
|
||||
/** Returns its argument. */
|
||||
export function identity<T>(x: T) { return x; }
|
||||
|
||||
|
|
|
@ -135,23 +135,34 @@ namespace ts {
|
|||
return configFile.options.rootDir || getDirectoryPath(Debug.assertDefined(configFile.options.configFilePath));
|
||||
}
|
||||
|
||||
function getOutputPathWithoutChangingExt(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean, outputDir: string | undefined) {
|
||||
return outputDir ?
|
||||
resolvePath(
|
||||
outputDir,
|
||||
getRelativePathFromDirectory(rootDirOfOptions(configFile), inputFileName, ignoreCase)
|
||||
) :
|
||||
inputFileName;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function getOutputDeclarationFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean) {
|
||||
Debug.assert(!fileExtensionIs(inputFileName, Extension.Dts) && hasTSFileExtension(inputFileName));
|
||||
const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile), inputFileName, ignoreCase);
|
||||
const outputPath = resolvePath(configFile.options.declarationDir || configFile.options.outDir || getDirectoryPath(Debug.assertDefined(configFile.options.configFilePath)), relativePath);
|
||||
return changeExtension(outputPath, Extension.Dts);
|
||||
return changeExtension(
|
||||
getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.declarationDir || configFile.options.outDir),
|
||||
Extension.Dts
|
||||
);
|
||||
}
|
||||
|
||||
function getOutputJSFileName(inputFileName: string, configFile: ParsedCommandLine, ignoreCase: boolean) {
|
||||
const relativePath = getRelativePathFromDirectory(rootDirOfOptions(configFile), inputFileName, ignoreCase);
|
||||
const outputPath = resolvePath(configFile.options.outDir || getDirectoryPath(Debug.assertDefined(configFile.options.configFilePath)), relativePath);
|
||||
const isJsonFile = fileExtensionIs(inputFileName, Extension.Json);
|
||||
const outputFileName = changeExtension(outputPath, isJsonFile ?
|
||||
Extension.Json :
|
||||
fileExtensionIs(inputFileName, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve ?
|
||||
Extension.Jsx :
|
||||
Extension.Js);
|
||||
const outputFileName = changeExtension(
|
||||
getOutputPathWithoutChangingExt(inputFileName, configFile, ignoreCase, configFile.options.outDir),
|
||||
isJsonFile ?
|
||||
Extension.Json :
|
||||
fileExtensionIs(inputFileName, Extension.Tsx) && configFile.options.jsx === JsxEmit.Preserve ?
|
||||
Extension.Jsx :
|
||||
Extension.Js
|
||||
);
|
||||
return !isJsonFile || comparePaths(inputFileName, outputFileName, Debug.assertDefined(configFile.options.configFilePath), ignoreCase) !== Comparison.EqualTo ?
|
||||
outputFileName :
|
||||
undefined;
|
||||
|
@ -203,6 +214,8 @@ namespace ts {
|
|||
const jsFilePath = getOutputJSFileName(inputFileName, configFile, ignoreCase);
|
||||
if (jsFilePath) return jsFilePath;
|
||||
}
|
||||
const buildInfoPath = getOutputPathForBuildInfo(configFile.options);
|
||||
if (buildInfoPath) return buildInfoPath;
|
||||
return Debug.fail(`project ${configFile.options.configFilePath} expected to have at least one output`);
|
||||
}
|
||||
|
||||
|
@ -665,11 +678,12 @@ namespace ts {
|
|||
getCompilerOptions: () => config.options,
|
||||
getCurrentDirectory: () => host.getCurrentDirectory(),
|
||||
getNewLine: () => host.getNewLine(),
|
||||
getSourceFile: () => undefined,
|
||||
getSourceFileByPath: () => undefined,
|
||||
getSourceFile: returnUndefined,
|
||||
getSourceFileByPath: returnUndefined,
|
||||
getSourceFiles: () => sourceFilesForJsEmit,
|
||||
getLibFileFromReference: notImplemented,
|
||||
isSourceFileFromExternalLibrary: returnFalse,
|
||||
getResolvedProjectReferenceToRedirect: returnUndefined,
|
||||
writeFile: (name, text, writeByteOrderMark) => {
|
||||
switch (name) {
|
||||
case jsFilePath:
|
||||
|
@ -706,7 +720,7 @@ namespace ts {
|
|||
fileExists: f => host.fileExists(f),
|
||||
directoryExists: host.directoryExists && (f => host.directoryExists!(f)),
|
||||
useCaseSensitiveFileNames: () => host.useCaseSensitiveFileNames(),
|
||||
getProgramBuildInfo: () => undefined
|
||||
getProgramBuildInfo: returnUndefined
|
||||
};
|
||||
emitFiles(notImplementedResolver, emitHost, /*targetSourceFile*/ undefined, /*emitOnlyDtsFiles*/ false, getTransformers(config.options));
|
||||
return outputFiles;
|
||||
|
|
|
@ -3405,7 +3405,7 @@ namespace ts {
|
|||
export const nullTransformationContext: TransformationContext = {
|
||||
enableEmitNotification: noop,
|
||||
enableSubstitution: noop,
|
||||
endLexicalEnvironment: () => undefined,
|
||||
endLexicalEnvironment: returnUndefined,
|
||||
getCompilerOptions: notImplemented,
|
||||
getEmitHost: notImplemented,
|
||||
getEmitResolver: notImplemented,
|
||||
|
|
|
@ -979,7 +979,7 @@ namespace ts {
|
|||
|
||||
function getCommonSourceDirectory() {
|
||||
if (commonSourceDirectory === undefined) {
|
||||
const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, options, isSourceFileFromExternalLibrary));
|
||||
const emittedFiles = filter(files, file => sourceFileMayBeEmitted(file, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect));
|
||||
if (options.rootDir && checkSourceFilesBelongToPath(emittedFiles, options.rootDir)) {
|
||||
// If a rootDir is specified use it as the commonSourceDirectory
|
||||
commonSourceDirectory = getNormalizedAbsolutePath(options.rootDir, currentDirectory);
|
||||
|
@ -1425,6 +1425,7 @@ namespace ts {
|
|||
getSourceFiles: program.getSourceFiles,
|
||||
getLibFileFromReference: program.getLibFileFromReference,
|
||||
isSourceFileFromExternalLibrary,
|
||||
getResolvedProjectReferenceToRedirect,
|
||||
writeFile: writeFileCallback || (
|
||||
(fileName, data, writeByteOrderMark, onError, sourceFiles) => host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles)),
|
||||
isEmitBlocked,
|
||||
|
@ -2740,13 +2741,14 @@ namespace ts {
|
|||
|
||||
// List of collected files is complete; validate exhautiveness if this is a project with a file list
|
||||
if (options.composite) {
|
||||
const sourceFiles = files.filter(f => !f.isDeclarationFile);
|
||||
if (rootNames.length < sourceFiles.length) {
|
||||
const normalizedRootNames = rootNames.map(r => normalizePath(r).toLowerCase());
|
||||
for (const file of sourceFiles.map(f => normalizePath(f.path).toLowerCase())) {
|
||||
if (normalizedRootNames.indexOf(file) === -1) {
|
||||
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.File_0_is_not_in_project_file_list_Projects_must_list_all_files_or_use_an_include_pattern, file));
|
||||
}
|
||||
const rootPaths = rootNames.map(toPath);
|
||||
for (const file of files) {
|
||||
// Ignore declaration files
|
||||
if (file.isDeclarationFile) continue;
|
||||
// Ignore json file thats from project reference
|
||||
if (isJsonSourceFile(file) && getResolvedProjectReferenceToRedirect(file.fileName)) continue;
|
||||
if (rootPaths.indexOf(file.path) === -1) {
|
||||
programDiagnostics.add(createCompilerDiagnostic(Diagnostics.File_0_is_not_in_project_file_list_Projects_must_list_all_files_or_use_an_include_pattern, file.fileName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3158,7 +3160,7 @@ namespace ts {
|
|||
readFile: f => directoryStructureHost.readFile(f),
|
||||
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames(),
|
||||
getCurrentDirectory: () => host.getCurrentDirectory(),
|
||||
onUnRecoverableConfigFileDiagnostic: host.onUnRecoverableConfigFileDiagnostic || (() => undefined),
|
||||
onUnRecoverableConfigFileDiagnostic: host.onUnRecoverableConfigFileDiagnostic || returnUndefined,
|
||||
trace: host.trace ? (s) => host.trace!(s) : undefined
|
||||
};
|
||||
}
|
||||
|
|
|
@ -339,7 +339,7 @@ namespace ts {
|
|||
|
||||
function createSolutionBuilderHostBase<T extends BuilderProgram>(system: System, createProgram: CreateProgram<T> | undefined, reportDiagnostic?: DiagnosticReporter, reportSolutionBuilderStatus?: DiagnosticReporter) {
|
||||
const host = createProgramHost(system, createProgram) as SolutionBuilderHostBase<T>;
|
||||
host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : () => undefined;
|
||||
host.getModifiedTime = system.getModifiedTime ? path => system.getModifiedTime!(path) : returnUndefined;
|
||||
host.setModifiedTime = system.setModifiedTime ? (path, date) => system.setModifiedTime!(path, date) : noop;
|
||||
host.deleteFile = system.deleteFile ? path => system.deleteFile!(path) : noop;
|
||||
host.reportDiagnostic = reportDiagnostic || createDiagnosticReporter(system);
|
||||
|
@ -660,15 +660,16 @@ namespace ts {
|
|||
}
|
||||
}
|
||||
|
||||
// Collect the expected outputs of this project
|
||||
const outputs = getAllProjectOutputs(project, !host.useCaseSensitiveFileNames());
|
||||
|
||||
if (outputs.length === 0) {
|
||||
// Container if no files are specified in the project
|
||||
if (!project.fileNames.length && !canJsonReportNoInutFiles(project.raw)) {
|
||||
return {
|
||||
type: UpToDateStatusType.ContainerOnly
|
||||
};
|
||||
}
|
||||
|
||||
// Collect the expected outputs of this project
|
||||
const outputs = getAllProjectOutputs(project, !host.useCaseSensitiveFileNames());
|
||||
|
||||
// Now see if all outputs are newer than the newest input
|
||||
let oldestOutputFileName = "(none)";
|
||||
let oldestOutputFileTime = maximumDate;
|
||||
|
|
|
@ -5337,6 +5337,7 @@ namespace ts {
|
|||
getCurrentDirectory(): string;
|
||||
|
||||
isSourceFileFromExternalLibrary(file: SourceFile): boolean;
|
||||
getResolvedProjectReferenceToRedirect(fileName: string): ResolvedProjectReference | undefined;
|
||||
getLibFileFromReference(ref: FileReference): SourceFile | undefined;
|
||||
|
||||
getCommonSourceDirectory(): string;
|
||||
|
|
|
@ -3422,22 +3422,31 @@ namespace ts {
|
|||
export function getSourceFilesToEmit(host: EmitHost, targetSourceFile?: SourceFile): ReadonlyArray<SourceFile> {
|
||||
const options = host.getCompilerOptions();
|
||||
const isSourceFileFromExternalLibrary = (file: SourceFile) => host.isSourceFileFromExternalLibrary(file);
|
||||
const getResolvedProjectReferenceToRedirect = (fileName: string) => host.getResolvedProjectReferenceToRedirect(fileName);
|
||||
if (options.outFile || options.out) {
|
||||
const moduleKind = getEmitModuleKind(options);
|
||||
const moduleEmitEnabled = options.emitDeclarationOnly || moduleKind === ModuleKind.AMD || moduleKind === ModuleKind.System;
|
||||
// Can emit only sources that are not declaration file and are either non module code or module with --module or --target es6 specified
|
||||
return filter(host.getSourceFiles(), sourceFile =>
|
||||
(moduleEmitEnabled || !isExternalModule(sourceFile)) && sourceFileMayBeEmitted(sourceFile, options, isSourceFileFromExternalLibrary));
|
||||
(moduleEmitEnabled || !isExternalModule(sourceFile)) && sourceFileMayBeEmitted(sourceFile, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect));
|
||||
}
|
||||
else {
|
||||
const sourceFiles = targetSourceFile === undefined ? host.getSourceFiles() : [targetSourceFile];
|
||||
return filter(sourceFiles, sourceFile => sourceFileMayBeEmitted(sourceFile, options, isSourceFileFromExternalLibrary));
|
||||
return filter(sourceFiles, sourceFile => sourceFileMayBeEmitted(sourceFile, options, isSourceFileFromExternalLibrary, getResolvedProjectReferenceToRedirect));
|
||||
}
|
||||
}
|
||||
|
||||
/** Don't call this for `--outFile`, just for `--outDir` or plain emit. `--outFile` needs additional checks. */
|
||||
export function sourceFileMayBeEmitted(sourceFile: SourceFile, options: CompilerOptions, isSourceFileFromExternalLibrary: (file: SourceFile) => boolean) {
|
||||
return !(options.noEmitForJsFiles && isSourceFileJS(sourceFile)) && !sourceFile.isDeclarationFile && !isSourceFileFromExternalLibrary(sourceFile);
|
||||
export function sourceFileMayBeEmitted(
|
||||
sourceFile: SourceFile,
|
||||
options: CompilerOptions,
|
||||
isSourceFileFromExternalLibrary: (file: SourceFile) => boolean,
|
||||
getResolvedProjectReferenceToRedirect: (fileName: string) => ResolvedProjectReference | undefined
|
||||
) {
|
||||
return !(options.noEmitForJsFiles && isSourceFileJS(sourceFile)) &&
|
||||
!sourceFile.isDeclarationFile &&
|
||||
!isSourceFileFromExternalLibrary(sourceFile) &&
|
||||
!(isJsonSourceFile(sourceFile) && getResolvedProjectReferenceToRedirect(sourceFile.fileName));
|
||||
}
|
||||
|
||||
export function getSourceFilePathInNewDir(fileName: string, host: EmitHost, newDirPath: string): string {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
namespace ts {
|
||||
describe("unittests:: tsbuild:: with resolveJsonModule option", () => {
|
||||
describe("unittests:: tsbuild:: with resolveJsonModule option on project resolveJsonModuleAndComposite", () => {
|
||||
let projFs: vfs.FileSystem;
|
||||
const { time, tick } = getTime();
|
||||
const allExpectedOutputs = ["/src/dist/src/index.js", "/src/dist/src/index.d.ts", "/src/dist/src/hello.json"];
|
||||
|
@ -64,19 +64,89 @@ export default hello.hello`);
|
|||
const configFile = "src/tsconfig_withFiles.json";
|
||||
replaceText(fs, configFile, `"composite": true,`, `"composite": true, "sourceMap": true,`);
|
||||
const host = new fakes.SolutionBuilderHost(fs);
|
||||
const builder = createSolutionBuilder(host, [configFile], { verbose: false });
|
||||
const builder = createSolutionBuilder(host, [configFile], { verbose: true });
|
||||
builder.buildAllProjects();
|
||||
host.assertDiagnosticMessages();
|
||||
host.assertDiagnosticMessages(
|
||||
getExpectedDiagnosticForProjectsInBuild(configFile),
|
||||
[Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFile, "src/dist/src/index.js"],
|
||||
[Diagnostics.Building_project_0, `/${configFile}`]
|
||||
);
|
||||
for (const output of [...allExpectedOutputs, "/src/dist/src/index.js.map"]) {
|
||||
assert(fs.existsSync(output), `Expect file ${output} to exist`);
|
||||
}
|
||||
host.clearDiagnostics();
|
||||
builder.resetBuildContext();
|
||||
tick();
|
||||
const newBuilder = createSolutionBuilder(host, [configFile], { verbose: true });
|
||||
newBuilder.buildAllProjects();
|
||||
builder.buildAllProjects();
|
||||
host.assertDiagnosticMessages(
|
||||
getExpectedDiagnosticForProjectsInBuild(configFile),
|
||||
[Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFile, "src/src/index.ts", "src/dist/src/index.js"]
|
||||
);
|
||||
});
|
||||
|
||||
it("with resolveJsonModule and without outDir", () => {
|
||||
const fs = projFs.shadow();
|
||||
const configFile = "src/tsconfig_withFiles.json";
|
||||
replaceText(fs, configFile, `"outDir": "dist",`, "");
|
||||
const host = new fakes.SolutionBuilderHost(fs);
|
||||
const builder = createSolutionBuilder(host, [configFile], { verbose: true });
|
||||
builder.buildAllProjects();
|
||||
host.assertDiagnosticMessages(
|
||||
getExpectedDiagnosticForProjectsInBuild(configFile),
|
||||
[Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, configFile, "src/src/index.js"],
|
||||
[Diagnostics.Building_project_0, `/${configFile}`]
|
||||
);
|
||||
for (const output of ["/src/src/index.js", "/src/src/index.d.ts"]) {
|
||||
assert(fs.existsSync(output), `Expect file ${output} to exist`);
|
||||
}
|
||||
host.clearDiagnostics();
|
||||
builder.resetBuildContext();
|
||||
tick();
|
||||
builder.buildAllProjects();
|
||||
host.assertDiagnosticMessages(
|
||||
getExpectedDiagnosticForProjectsInBuild(configFile),
|
||||
[Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, configFile, "src/src/index.ts", "src/src/index.js"]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("unittests:: tsbuild:: with resolveJsonModule option on project importJsonFromProjectReference", () => {
|
||||
const { time, tick } = getTime();
|
||||
let projFs: vfs.FileSystem;
|
||||
before(() => {
|
||||
projFs = loadProjectFromDisk("tests/projects/importJsonFromProjectReference", time);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
projFs = undefined!; // Release the contents
|
||||
});
|
||||
|
||||
it("when importing json module from project reference", () => {
|
||||
const expectedOutput = "/src/main/index.js";
|
||||
const fs = projFs.shadow();
|
||||
const configFile = "src/tsconfig.json";
|
||||
const stringsConfigFile = "src/strings/tsconfig.json";
|
||||
const mainConfigFile = "src/main/tsconfig.json";
|
||||
const host = new fakes.SolutionBuilderHost(fs);
|
||||
const builder = createSolutionBuilder(host, [configFile], { verbose: true });
|
||||
builder.buildAllProjects();
|
||||
host.assertDiagnosticMessages(
|
||||
getExpectedDiagnosticForProjectsInBuild(stringsConfigFile, mainConfigFile, configFile),
|
||||
[Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, stringsConfigFile, "src/strings/tsconfig.tsbuildinfo"],
|
||||
[Diagnostics.Building_project_0, `/${stringsConfigFile}`],
|
||||
[Diagnostics.Project_0_is_out_of_date_because_output_file_1_does_not_exist, mainConfigFile, "src/main/index.js"],
|
||||
[Diagnostics.Building_project_0, `/${mainConfigFile}`],
|
||||
);
|
||||
assert(fs.existsSync(expectedOutput), `Expect file ${expectedOutput} to exist`);
|
||||
host.clearDiagnostics();
|
||||
builder.resetBuildContext();
|
||||
tick();
|
||||
builder.buildAllProjects();
|
||||
host.assertDiagnosticMessages(
|
||||
getExpectedDiagnosticForProjectsInBuild(stringsConfigFile, mainConfigFile, configFile),
|
||||
[Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, stringsConfigFile, "src/strings/foo.json", "src/strings/tsconfig.tsbuildinfo"],
|
||||
[Diagnostics.Project_0_is_up_to_date_because_newest_input_1_is_older_than_oldest_output_2, mainConfigFile, "src/main/index.ts", "src/main/index.js"],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@ namespace ts.projectSystem {
|
|||
msg: noop,
|
||||
startGroup: noop,
|
||||
endGroup: noop,
|
||||
getLogFileName: () => undefined,
|
||||
getLogFileName: returnUndefined,
|
||||
};
|
||||
|
||||
export function createHasErrorMessageLogger() {
|
||||
|
|
|
@ -216,7 +216,7 @@ namespace ts.projectSystem {
|
|||
},
|
||||
startGroup: noop,
|
||||
endGroup: noop,
|
||||
getLogFileName: () => undefined
|
||||
getLogFileName: returnUndefined
|
||||
};
|
||||
return {
|
||||
errorLogger,
|
||||
|
|
|
@ -8,7 +8,7 @@ namespace ts.server {
|
|||
newLine: "\n",
|
||||
useCaseSensitiveFileNames: true,
|
||||
write(s): void { lastWrittenToHost = s; },
|
||||
readFile: () => undefined,
|
||||
readFile: returnUndefined,
|
||||
writeFile: noop,
|
||||
resolvePath(): string { return undefined!; }, // TODO: GH#18217
|
||||
fileExists: () => false,
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { foo } from '../strings/foo.json';
|
||||
|
||||
console.log(foo);
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../strings/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"foo": "bar baz"
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [ "foo.json" ],
|
||||
"references": []
|
||||
}
|
20
tests/projects/importJsonFromProjectReference/tsconfig.json
Normal file
20
tests/projects/importJsonFromProjectReference/tsconfig.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"rootDir": "./",
|
||||
"composite": true,
|
||||
"resolveJsonModule": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"references": [
|
||||
{
|
||||
"path": "./strings/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./main/tsconfig.json"
|
||||
}
|
||||
],
|
||||
"files": []
|
||||
}
|
Loading…
Reference in a new issue