Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(ui5-tooling-transpile): add support for d.ts source maps #735

Merged
merged 1 commit into from
Apr 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 96 additions & 36 deletions packages/ui5-tooling-transpile/lib/task.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,13 @@ module.exports = async function ({ workspace /*, dependencies*/, taskUtil, optio
// read source file
const source = await resource.getString();

// store the ts source code in the sources map
// store the source code in the sources map
if (config.transformTypeScript) {
sourcesMap[resourcePath] = source;
}

// we ignore d.ts files for transpiling
if (!resourcePath.endsWith(".d.ts")) {
// mark TypeScript source for omit from build result
if (resourcePath.endsWith(".ts")) {
taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult, true);
}

// transpile the source
config.debug && log.info(`Transpiling resource ${resourcePath}`);
const result = await transformAsync(
Expand All @@ -70,41 +65,61 @@ module.exports = async function ({ workspace /*, dependencies*/, taskUtil, optio
);

// create the ts file in the workspace
config.debug && log.info(` + [.js] ${filePath}`);
const transpiledResource = resourceFactory.createResource({
path: filePath,
string: normalizeLineFeeds(result.code)
});
workspace.write(transpiledResource);
await workspace.write(transpiledResource);

// create sourcemap resource if available
if (result.map) {
result.map.file = path.basename(filePath);
config.debug && log.info(` + sourcemap ${filePath}.map`);
config.debug && log.info(` + [.js.map] ${filePath}.map`);

const resourceMap = resourceFactory.createResource({
path: `${filePath}.map`,
string: JSON.stringify(result.map)
});

workspace.write(resourceMap);
await workspace.write(resourceMap);
}
}
}
})
);

// generate the dts files for the ts files
if (config.transformTypeScript) {
// generate the d.ts(.map)? files for the ts files via TSC API
// for the resources of the root project (not included dependencies)
if (config.transformTypeScript && taskUtil.isRootProject()) {
// determine if the project is a library and enable the DTS generation by default
// TODO: UI5 Tooling 3.0 allows to access the project with the TaskUtil
// https://sap.github.io/ui5-tooling/v3/api/@ui5_project_build_helpers_TaskUtil.html#~ProjectInterface
// from here we could derive the project type instead of guessing via file existence
const libraryResources = await workspace.byGlob(`/resources/${options.projectNamespace}/*library*`);
if (libraryResources.length > 0 && config.generateDts === undefined) {
const isLibrary = libraryResources.length > 0 && config.generateDts === undefined;
if (isLibrary) {
config.debug && log.info(`Enabling d.ts generation by default for library projects!`);
config.generateDts = true;
}

// omit resources from build result (unfortunately no better place found which
// allows doing this during the iteration across all resources at another place...)
for await (const resourcePath of Object.keys(sourcesMap)) {
// all ts files will be omitted from the build result
let omitFromBuildResult = resourcePath.endsWith(".ts");
// root projects with generateDts=true will include d.ts files for build result
if (resourcePath.endsWith(".d.ts")) {
omitFromBuildResult = !taskUtil.isRootProject() || !config.generateDts;
}
// omit the resource from the build result
if (omitFromBuildResult) {
config.debug && log.verbose(`Omitting resource ${resourcePath}`);
const resource = await workspace.byPath(resourcePath);
taskUtil.setTag(resource, taskUtil.STANDARD_TAGS.OmitFromBuildResult, true);
}
}

// generate the dts files for the ts files
if (config.generateDts) {
try {
Expand All @@ -115,13 +130,15 @@ module.exports = async function ({ workspace /*, dependencies*/, taskUtil, optio
const options = {
allowJs: true,
declaration: true,
//declarationMap: true,
emitDeclarationOnly: true
declarationMap: true,
emitDeclarationOnly: true,
sourceRoot: "."
//traceResolution: true,
};

// update the sources map to declare the modules with the full module name
for await (const resourcePath of Object.keys(sourcesMap)) {
// declare the modules with its namespace
const source = sourcesMap[resourcePath];
let moduleName = /^\/resources\/(.*)\.ts$/.exec(resourcePath)?.[1];
if (moduleName?.endsWith(".gen.d")) {
Expand All @@ -135,24 +152,57 @@ module.exports = async function ({ workspace /*, dependencies*/, taskUtil, optio
)}`;
const resource = await workspace.byPath(resourcePath);
resource.setString(sourcesMap[resourcePath]);
workspace.write(resource);
await workspace.write(resource);
} else if (moduleName) {
sourcesMap[resourcePath] = `declare module "${moduleName}" {\n${source}\n}`;
}
}

// Create a Program with an in-memory emit
const host = ts.createCompilerHost(options);
(host.getCurrentDirectory = () => ""),
(host.fileExists = (file) => !!sourcesMap[file] || fs.existsSync(file));
host.readFile = (file) => sourcesMap[file] || fs.readFileSync(file, "utf-8");
host.writeFile = function (fileName, content) {
config.debug && log.info(`Generating d.ts for resource ${fileName}`);
// array of promises (d.ts generation) to await them later
const dtsFilePromises = [];
const writeDtsFile = function (fileName, content) {
const dtsFile = resourceFactory.createResource({
path: `${fileName}`,
string: content
});
workspace.write(dtsFile);
dtsFilePromises.push(workspace.write(dtsFile));
};

// emit type definitions in-memory and read/write resources from the UI5 workspace
const host = ts.createCompilerHost(options);
(host.getCurrentDirectory = () => ""),
(host.fileExists = (file) => !!sourcesMap[file] || fs.existsSync(file));
host.readFile = (file) => sourcesMap[file] || fs.readFileSync(file, "utf-8");
host.writeFile = function (fileName, content, writeByteOrderMark, onError, sourceFiles /*, data*/) {
const sourceFile = sourceFiles[0]; // we typically only have one source file!
config.debug && log.info(` + [${/(\.d\.ts(?:\.map)?)$/.exec(fileName)[0]}] ${fileName}`);
if (/\.d\.ts\.map$/.test(fileName)) {
// for d.ts.map we need to fix the sources mapping in order to be
// able to use the "Go to Source Definition" feature of VSCode
// /!\ this solution is fragile as it assumes to be generated
// into a direct folder (like dist) and not in deeper structures
// -> to avoid the hack we need more FS infos from the tooling!
try {
const resourcePath = /^\//.test(sourceFile.fileName)
? sourceFile.fileName
: `/${sourceFile.fileName}`;
workspace.byPath(resourcePath).then((resource) => {
const jsonContent = JSON.parse(content);
// libs build into namespace (resolve), applications into root (assume "..") in dist folder!
jsonContent.sourceRoot = isLibrary ? path.relative(resource.getPath(), "/") : "..";
jsonContent.sources = [path.relative(process.cwd(), determineResourceFSPath(resource))];
writeDtsFile(fileName, JSON.stringify(jsonContent));
});
} catch (e) {
config.debug &&
log.warn(` /!\\ Failed to patch sources information of ${fileName}. Reason: ${e}`);
// as this is a hack, we can fallback to the by default
// generated sources information of the ts builder
writeDtsFile(fileName, content);
}
} else {
writeDtsFile(fileName, content);
}
};
host.resolveModuleNames = function (moduleNames, containingFile) {
const resolvedModules = [];
Expand Down Expand Up @@ -183,19 +233,21 @@ module.exports = async function ({ workspace /*, dependencies*/, taskUtil, optio
const program = ts.createProgram(Object.keys(sourcesMap), options, host);
const result = program.emit();

// error diagnostics
if (result.emitSkipped) {
log.error(
`The following errors occured during d.ts generation: \n${ts.formatDiagnostics(
result.diagnostics,
host
)}`
);
}
// wait until all files are d.ts(.map)? written
await Promise.all(dtsFilePromises);

// create the index.d.ts in the root output folder
if (taskUtil.isRootProject()) {
config.debug && log.info(`Generating index.d.ts`);
if (!result.emitSkipped) {
// create the index.d.ts in the root output folder
config.debug && log.info(` + [.d.ts] index.d.ts`);
const pckgJsonFile = path.join(process.cwd(), "package.json");
if (fs.existsSync(pckgJsonFile)) {
const pckgJson = require(pckgJsonFile);
if (!pckgJson.types) {
log.warn(
` /!\\ package.json has no "types" property! Add it and point to "index.d.ts" in build destination!`
);
}
}
const indexDtsContent = Object.keys(sourcesMap)
.filter((dtsFile) => dtsFile.startsWith("/resources/"))
.map(
Expand All @@ -209,7 +261,15 @@ module.exports = async function ({ workspace /*, dependencies*/, taskUtil, optio
path: `/index.d.ts`,
string: indexDtsContent
});
workspace.write(indexDtsFile);
await workspace.write(indexDtsFile);
} else {
// error diagnostics
log.error(
`The following errors occured during d.ts generation: \n${ts.formatDiagnostics(
result.diagnostics,
host
)}`
);
}
} catch (e) {
// typescript dependency should be available, otherwise we can't generate the dts files
Expand Down
1 change: 1 addition & 0 deletions showcases/ui5-tsapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"private": true,
"author": "Jorge Martins, Peter Muessig",
"license": "Apache-2.0",
"types": "dist/index.d.ts",
"scripts": {
"clean": "rimraf dist coverage",
"build": "ui5 build --clean-dest",
Expand Down
5 changes: 2 additions & 3 deletions showcases/ui5-tsapp/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
"paths": {
"ui5/ecosystem/demo/tsapp/*": ["./webapp/*"],
"unit/*": ["./webapp/test/unit/*"],
"integration/*": ["./webapp/test/integration/*"],
"ui5/ecosystem/demo/tslib/*": ["./node_modules/ui5-tslib/src/ui5/ecosystem/demo/tslib/*"]
"integration/*": ["./webapp/test/integration/*"]
},
"typeRoots": ["./node_modules/@types", "./node_modules/ui5-tslib"]
"types": ["openui5", "ui5-tslib"]
},
"include": ["./webapp/**/*.ts"]
}
1 change: 1 addition & 0 deletions showcases/ui5-tslib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"private": true,
"author": "Peter Muessig",
"license": "Apache-2.0",
"types": "dist/index.d.ts",
"scripts": {
"clean": "rimraf dist coverage",
"build": "npm run build:controls && ui5 build --clean-dest",
Expand Down