Skip to content

Commit

Permalink
feat: Provide support for type augmentation (#8534)
Browse files Browse the repository at this point in the history
Allowing for the extensibility of our build-in core models helps in implementing a da- ta-driven approach for both our own libraries and 3rd party ones.

As there are some well-known limitations in TypeScript around it, mentioned in those tickets:

microsoft/TypeScript#9532
microsoft/TypeScript#18877
Spartacus uses additional build step for our libraries that will move augmentable models to main entry point generated by ng-packagr (eg spartacus-core.d.ts for core).

Closes #7940
  • Loading branch information
dunqan authored Aug 11, 2020
1 parent ee00638 commit 3a98614
Show file tree
Hide file tree
Showing 10 changed files with 484 additions and 1 deletion.
2 changes: 1 addition & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@
"prefix": "cx",
"architect": {
"build": {
"builder": "@angular-devkit/build-ng-packagr:build",
"builder": "./tools/build-lib:augmented-types",
"options": {
"tsConfig": "projects/core/tsconfig.lib.json",
"project": "projects/core/ng-package.json"
Expand Down
5 changes: 5 additions & 0 deletions projects/core/public_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,8 @@ export * from './src/store-finder/index';
export * from './src/user/index';
export * from './src/util/index';
export * from './src/window/index';

/** AUGMENTABLE_TYPES_START */
export { Product } from './src/model/product.model';
export { ProductSearchPage, Facet } from './src/model/product-search.model';
/** AUGMENTABLE_TYPES_END */
17 changes: 17 additions & 0 deletions tools/build-lib/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Outputs
**/*.js.map
**/*.d.ts

# IDEs
.idea/
jsconfig.json
.vscode/

# Misc
node_modules/
npm-debug.log*
yarn-error.log*

# Mac OSX Finder files.
**/.DS_Store
.DS_Store
109 changes: 109 additions & 0 deletions tools/build-lib/augmented-types/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

151 changes: 151 additions & 0 deletions tools/build-lib/augmented-types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import {
BuilderContext,
BuilderOutput,
createBuilder,
} from '@angular-devkit/architect';
import { JsonObject, logging } from '@angular-devkit/core';
import { NgPackagrBuilderOptions } from '@angular-devkit/build-ng-packagr';
import { from, Observable, of } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { promises as fs } from 'fs';
import * as path from 'path';
import * as globModule from 'glob';
import { promisify } from 'util';
const glob = promisify(globModule);

const DELIMITER_START = '/** AUGMENTABLE_TYPES_START */';
const DELIMITER_END = '/** AUGMENTABLE_TYPES_END */';

export default createBuilder(augmentedTypesBuilder);

/**
* Builder that runs default ng-packagr builder ('@angular-devkit/build-ng-packagr:build')
* and performs additional post step to move augmentable types to root d.ts file.
*
* It's a workaround to make TS types augmentable, reference issues:
* - https://github.com/microsoft/TypeScript/issues/9532
* - https://github.com/microsoft/TypeScript/issues/18877
*/
function augmentedTypesBuilder(
options: NgPackagrBuilderOptions & JsonObject,
context: BuilderContext
): Observable<BuilderOutput> {
return from(ngPackagrBuild(context, options)).pipe(
switchMap((result) =>
result.success
? from(
augmentableTypesPostStep(
context,
options as NgPackagrBuilderOptions
)
)
: of(result)
)
);
}

/**
* Run ng packager build step as is
*/
async function ngPackagrBuild(
context: BuilderContext,
options: NgPackagrBuilderOptions & JsonObject
): Promise<BuilderOutput> {
const builderRun = await context.scheduleBuilder(
'@angular-devkit/build-ng-packagr:build',
options
);
return await builderRun.result;
}

/**
* Post build step
*/
async function augmentableTypesPostStep(
context: BuilderContext,
options: NgPackagrBuilderOptions
): Promise<BuilderOutput> {
const outputPath = await getNgPackgrLibOutputPath(options.project);
await propagateAugmentableTypes(outputPath, context.logger);
return { success: true };
}

/**
* Get output directory for ng packager job
* @param ngPackagerFile
*/
async function getNgPackgrLibOutputPath(ngPackagerFile: string) {
let ngPackageData = JSON.parse(await fs.readFile(ngPackagerFile, 'utf8'));
return path.join(path.dirname(ngPackagerFile), ngPackageData.dest);
}

/**
* Propagate augmentable types for every package.json file in the built in library
*/
async function propagateAugmentableTypes(
libPath: string,
logger: logging.LoggerApi
) {
// grab all package.json files
const files = await glob(libPath + '/**/package.json');

for (const packageJsonFile of files) {
try {
// get typings file from package.json
let packageData = JSON.parse(await fs.readFile(packageJsonFile, 'utf8'));
const typingsFile = packageData.typings;

if (!typingsFile) {
continue;
}
const typingsFilePath = path.join(libPath, typingsFile);
let typingsFileSource = await fs.readFile(typingsFilePath, 'utf8');

// look for export from public api file
const regex = /export \* from '(.+)\'/;
const publicApiFile = typingsFileSource.match(regex)![1];
const apiFilePath = path.join(libPath, publicApiFile + '.d.ts');

let publicApiFileSource = await fs.readFile(apiFilePath, 'utf8');

// find augmentable types delimiter in public api file
const augTypesStart = publicApiFileSource.indexOf(DELIMITER_START);

if (augTypesStart === -1) {
return;
}

const augTypesEnd =
publicApiFileSource.indexOf(DELIMITER_END) + DELIMITER_END.length + 1;

// extract augmentable types block
const augTypes = publicApiFileSource.substr(
augTypesStart,
augTypesEnd - augTypesStart
);
// remove augmentable types block from public api file
publicApiFileSource =
publicApiFileSource.substr(0, augTypesStart) +
publicApiFileSource.substr(augTypesEnd);

// incorporate augmentable types block into typings file
const firstExportPos = typingsFileSource.indexOf('export *');
typingsFileSource =
typingsFileSource.substr(0, firstExportPos) +
augTypes +
typingsFileSource.substr(firstExportPos);

// write results
await fs.writeFile(apiFilePath, publicApiFileSource, 'utf8');
await fs.writeFile(typingsFilePath, typingsFileSource, 'utf8');

logger.info(
'Propagated types from ' + apiFilePath + ' to ' + typingsFilePath
);
} catch (e) {
logger.error(
'Error when propagating augmentable types for ' + packageJsonFile
);
}
}
}
4 changes: 4 additions & 0 deletions tools/build-lib/augmented-types/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "http://json-schema.org/schema",
"type": "object"
}
9 changes: 9 additions & 0 deletions tools/build-lib/builders.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"builders": {
"augmented-types": {
"implementation": "./augmented-types",
"schema": "./augmented-types/schema.json",
"description": "Propagate typing exports to main d.ts file to make them augmentable."
}
}
}
21 changes: 21 additions & 0 deletions tools/build-lib/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@spartacus/build-lib",
"version": "0.1.0",
"description": "Angular Build Architect for Spartacus libraries",
"main": "augmented-types/index.js",
"builders": "builders.json",
"scripts": {
"build": "tsc"
},
"keywords": [],
"author": "",
"license": "Apache-2.0",
"devDependencies": {
"@types/node": "^12.12.27",
"ts-node": "^8.1.0",
"typescript": "^3.4.3"
},
"dependencies": {
"@angular-devkit/architect": "0.901.7"
}
}
20 changes: 20 additions & 0 deletions tools/build-lib/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"baseUrl": "tsconfig",
"target": "es6",
"declaration": true,
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"skipDefaultLibCheck": true,
"skipLibCheck": true,
"sourceMap": true,
"strictNullChecks": true,
"types": ["node", "jasmine"]
}
}
Loading

0 comments on commit 3a98614

Please sign in to comment.