Skip to content
This repository was archived by the owner on Apr 4, 2025. It is now read-only.

Commit 539028c

Browse files
committed
build: add sdk-admin binary, bootstrap-local, ...
Includes unit testing script and bunch of unit testing configuration.
1 parent 6216570 commit 539028c

File tree

5 files changed

+365
-4
lines changed

5 files changed

+365
-4
lines changed

bin/sdk-admin

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/usr/bin/env node
2+
'use strict';
3+
4+
/**
5+
* This file is useful for not having to load bootstrap-local in various javascript.
6+
* Simply use package.json to have npm scripts that use this script as well, or use
7+
* this script directly.
8+
*/
9+
10+
require('../lib/bootstrap-local');
11+
12+
const minimist = require('minimist');
13+
14+
const args = minimist(process.argv.slice(2));
15+
16+
17+
switch (args._[0]) {
18+
case 'test': require('../scripts/test').default(args); break;
19+
20+
default: console.log('Unknown command: ' + args._[0]); break;
21+
}

lib/bootstrap-local.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/* eslint-disable no-console */
2+
'use strict';
3+
4+
const fs = require('fs');
5+
const path = require('path');
6+
const ts = require('typescript');
7+
8+
9+
Error.stackTraceLimit = Infinity;
10+
11+
global._SdkIsLocal = true;
12+
global._SdkRoot = path.resolve(__dirname, '..');
13+
global._SdkPackages = require('./packages').packages;
14+
global._SdkTools = require('./packages').tools;
15+
16+
global._SdkRequireHook = null;
17+
18+
19+
const compilerOptions = ts.readConfigFile(path.join(__dirname, '../tsconfig.json'), p => {
20+
return fs.readFileSync(p, 'utf-8');
21+
}).config;
22+
23+
24+
const oldRequireTs = require.extensions['.ts'];
25+
require.extensions['.ts'] = function (m, filename) {
26+
// If we're in node module, either call the old hook or simply compile the
27+
// file without transpilation. We do not touch node_modules/**.
28+
// We do touch `Angular SDK` files anywhere though.
29+
if (!filename.match(/@angular\/cli\b/) && filename.match(/node_modules/)) {
30+
if (oldRequireTs) {
31+
return oldRequireTs(m, filename);
32+
}
33+
return m._compile(fs.readFileSync(filename), filename);
34+
}
35+
36+
// Node requires all require hooks to be sync.
37+
const source = fs.readFileSync(filename).toString();
38+
39+
try {
40+
let result = ts.transpile(source, compilerOptions['compilerOptions'], filename);
41+
42+
if (global._SdkRequireHook) {
43+
result = global._SdkRequireHook(result, filename);
44+
}
45+
46+
// Send it to node to execute.
47+
return m._compile(result, filename);
48+
} catch (err) {
49+
console.error('Error while running script "' + filename + '":');
50+
console.error(err.stack);
51+
throw err;
52+
}
53+
};
54+
55+
56+
// If we're running locally, meaning npm linked. This is basically "developer mode".
57+
if (!__dirname.match(new RegExp(`\\${path.sep}node_modules\\${path.sep}`))) {
58+
const packages = require('./packages').packages;
59+
60+
// We mock the module loader so that we can fake our packages when running locally.
61+
const Module = require('module');
62+
const oldLoad = Module._load;
63+
const oldResolve = Module._resolveFilename;
64+
65+
Module._resolveFilename = function (request, parent) {
66+
if (request in packages) {
67+
return packages[request].main;
68+
} else if (request.startsWith('@angular/cli/')) {
69+
// We allow deep imports (for now).
70+
// TODO: move tests to inside @angular/cli package so they don't have to deep import.
71+
const dir = path.dirname(parent.filename);
72+
return path.relative(dir, path.join(__dirname, '../packages', request));
73+
} else {
74+
let match = Object.keys(packages).find(pkgName => request.startsWith(pkgName + '/'));
75+
if (match) {
76+
const p = path.join(packages[match].root, request.substr(match.length));
77+
return oldResolve.call(this, p, parent);
78+
} else {
79+
return oldResolve.apply(this, arguments);
80+
}
81+
}
82+
};
83+
}

lib/packages.js

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
'use strict';
2+
3+
const fs = require('fs');
4+
const glob = require('glob');
5+
const path = require('path');
6+
7+
const packageRoot = path.join(__dirname, '../packages');
8+
const toolsRoot = path.join(__dirname, '../tools');
9+
const distRoot = path.join(__dirname, '../dist');
10+
11+
12+
// All the supported packages. Go through the packages directory and create a _map of
13+
// name => fullPath.
14+
const packages =
15+
[].concat(
16+
glob.sync(path.join(packageRoot, '*/package.json')),
17+
glob.sync(path.join(packageRoot, '*/*/package.json')))
18+
.filter(p => !p.match(/blueprints/))
19+
.map(pkgPath => path.relative(packageRoot, path.dirname(pkgPath)))
20+
.map(pkgName => {
21+
return { name: pkgName, root: path.join(packageRoot, pkgName) };
22+
})
23+
.reduce((packages, pkg) => {
24+
let pkgJson = JSON.parse(fs.readFileSync(path.join(pkg.root, 'package.json'), 'utf8'));
25+
let name = pkgJson['name'];
26+
let bin = {};
27+
Object.keys(pkgJson['bin'] || {}).forEach(binName => {
28+
bin[binName] = path.resolve(pkg.root, pkgJson['bin'][binName]);
29+
});
30+
31+
packages[name] = {
32+
dist: path.join(distRoot, pkg.name),
33+
packageJson: path.join(pkg.root, 'package.json'),
34+
root: pkg.root,
35+
relative: path.relative(path.dirname(__dirname), pkg.root),
36+
main: path.resolve(pkg.root, 'src/index.ts'),
37+
bin: bin
38+
};
39+
return packages;
40+
}, {});
41+
42+
const tools = glob.sync(path.join(toolsRoot, '**/package.json'))
43+
.map(toolPath => path.relative(toolsRoot, path.dirname(toolPath)))
44+
.map(toolName => {
45+
const root = path.join(toolsRoot, toolName);
46+
const pkgJson = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
47+
const name = pkgJson['name'];
48+
const dist = path.join(distRoot, toolName);
49+
50+
return {
51+
name,
52+
main: path.join(dist, pkgJson['main']),
53+
mainTs: path.join(toolsRoot, toolName, pkgJson['main'].replace(/\.js$/, '.ts')),
54+
root,
55+
packageJson: path.join(toolsRoot, toolName, 'package.json'),
56+
dist
57+
};
58+
})
59+
.reduce((tools, tool) => {
60+
tools[tool.name] = tool;
61+
return tools;
62+
}, {});
63+
64+
65+
module.exports = { packages, tools };

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
"version": "0.0.0",
44
"description": "Software Development Kit for Angular",
55
"bin": {
6-
"sdk-admin": "./bin/sdk-admin",
7-
"schematics": "./bin/schematics"
6+
"sdk-admin": "./bin/sdk-admin"
87
},
98
"keywords": [],
109
"scripts": {
11-
"build": "sdk-admin build",
12-
"test": "sdk-admin test"
10+
"test": "./bin/sdk-admin test"
1311
},
1412
"repository": {
1513
"type": "git",

scripts/test.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import 'jasmine';
2+
3+
import * as glob from 'glob';
4+
import * as Istanbul from 'istanbul';
5+
import {SpecReporter as JasmineSpecReporter} from 'jasmine-spec-reporter';
6+
import {join, relative} from 'path';
7+
import {Position, SourceMapConsumer, SourceMapGenerator} from 'source-map';
8+
9+
10+
const Jasmine = require('jasmine');
11+
12+
const projectBaseDir = join(__dirname, '../packages');
13+
require('source-map-support').install({
14+
hookRequire: true
15+
});
16+
17+
18+
declare let global: any & {
19+
__coverage__: any;
20+
};
21+
22+
const inlineSourceMapRe = /\/\/# sourceMappingURL=data:application\/json;base64,(\S+)$/;
23+
24+
25+
// Use the internal SDK Hook of the require extension installed by our bootstrapping code to add
26+
// Istanbul collection to the code.
27+
const codeMap = new Map<string, { code: string, map: SourceMapConsumer }>();
28+
29+
(global as any)._SdkRequireHook = function(code: string, filename: string) {
30+
// Skip spec files.
31+
if (filename.match(/_spec\.ts$/)) {
32+
return code;
33+
}
34+
if (codeMap.get(filename)) {
35+
return codeMap.get(filename)!.code;
36+
}
37+
38+
const instrumenter = new Istanbul.Instrumenter({
39+
esModules: true,
40+
codeGenerationOptions: {
41+
sourceMap: filename,
42+
sourceMapWithCode: true
43+
}
44+
});
45+
let instrumentedCode = instrumenter.instrumentSync(code, filename);
46+
const sourceMapGenerator: SourceMapGenerator = (instrumenter as any).sourceMap;
47+
const match = code.match(inlineSourceMapRe);
48+
49+
if (match) {
50+
// Fix source maps for exception reporting (since the exceptions happen in the instrumented
51+
// code.
52+
const sourceMapJson = JSON.parse(Buffer.from(match[1], 'base64').toString());
53+
const consumer = new SourceMapConsumer(sourceMapJson);
54+
sourceMapGenerator.applySourceMap(consumer, filename);
55+
56+
instrumentedCode = instrumentedCode.replace(inlineSourceMapRe, '')
57+
+ '//# sourceMappingURL=data:application/json;base64,'
58+
+ new Buffer(sourceMapGenerator.toString()).toString('base64');
59+
60+
// Keep the consumer from the original source map, because the reports from Istanbul are
61+
// already mapped against the code.
62+
codeMap.set(filename, { code: instrumentedCode, map: consumer });
63+
}
64+
65+
return instrumentedCode;
66+
};
67+
68+
69+
// Add the Istanbul reporter.
70+
const istanbulCollector = new Istanbul.Collector({});
71+
const istanbulReporter = new Istanbul.Reporter(undefined, 'coverage/');
72+
istanbulReporter.addAll(['json', 'lcov']);
73+
74+
75+
interface CoverageLocation {
76+
start: Position;
77+
end: Position;
78+
}
79+
80+
class IstanbulReporter implements jasmine.CustomReporter {
81+
// Update a location object from a SourceMap. Will ignore the location if the sourcemap does
82+
// not have a valid mapping.
83+
private _updateLocation(consumer: SourceMapConsumer, location: CoverageLocation) {
84+
const start = consumer.originalPositionFor(location.start);
85+
const end = consumer.originalPositionFor(location.end);
86+
87+
// Filter invalid original positions.
88+
if (start.line !== null && start.column !== null) {
89+
// Filter unwanted properties.
90+
location.start = { line: start.line, column: start.column };
91+
}
92+
if (end.line !== null && end.column !== null) {
93+
location.end = { line: end.line, column: end.column };
94+
}
95+
}
96+
97+
private _updateCoverageJsonSourceMap(coverageJson: any) {
98+
// Update the coverageJson with the SourceMap.
99+
for (const path of Object.keys(coverageJson)) {
100+
const entry = codeMap.get(path);
101+
if (!entry) {
102+
continue;
103+
}
104+
105+
const consumer = entry.map;
106+
const coverage = coverageJson[path];
107+
108+
// Update statement maps.
109+
for (const branchId of Object.keys(coverage.branchMap)) {
110+
debugger;
111+
const branch = coverage.branchMap[branchId];
112+
let line: number | null = null;
113+
let column = 0;
114+
do {
115+
line = consumer.originalPositionFor({ line: branch.line, column: column++ }).line;
116+
} while (line === null && column < 100);
117+
118+
branch.line = line;
119+
120+
for (const location of branch.locations) {
121+
this._updateLocation(consumer, location);
122+
}
123+
}
124+
125+
for (const id of Object.keys(coverage.statementMap)) {
126+
const location = coverage.statementMap[id];
127+
this._updateLocation(consumer, location);
128+
}
129+
130+
for (const id of Object.keys(coverage.fnMap)) {
131+
const fn = coverage.fnMap[id];
132+
fn.line = consumer.originalPositionFor({ line: fn.line, column: 0 }).line;
133+
this._updateLocation(consumer, fn.loc);
134+
}
135+
}
136+
}
137+
138+
jasmineDone(_runDetails: jasmine.RunDetails): void {
139+
if (global.__coverage__) {
140+
this._updateCoverageJsonSourceMap(global.__coverage__);
141+
istanbulCollector.add(global.__coverage__);
142+
}
143+
144+
istanbulReporter.write(istanbulCollector, true, () => {});
145+
}
146+
}
147+
148+
149+
// Create a Jasmine runner and configure it.
150+
const runner = new Jasmine({ projectBaseDir: projectBaseDir });
151+
152+
if (process.argv.indexOf('--spec-reporter') != -1) {
153+
runner.env.clearReporters();
154+
runner.env.addReporter(new JasmineSpecReporter({
155+
stacktrace: {
156+
// Filter all JavaScript files that appear after a TypeScript file (callers) from the stack
157+
// trace.
158+
filter: (x: string) => {
159+
return x.substr(0, x.indexOf('\n', x.indexOf('\n', x.lastIndexOf('.ts:')) + 1));
160+
}
161+
},
162+
suite: {
163+
displayNumber: true
164+
},
165+
summary: {
166+
displayStacktrace: true,
167+
displayErrorMessages: true
168+
}
169+
}));
170+
}
171+
runner.env.addReporter(new IstanbulReporter());
172+
173+
174+
// Manually set exit code (needed with custom reporters)
175+
runner.onComplete((success: boolean) => {
176+
process.exitCode = success ? 0 : 1;
177+
});
178+
179+
180+
glob.sync('packages/**/*.spec.ts')
181+
.filter(p => !/schematics_cli\/schematics\//.test(p))
182+
.forEach(path => {
183+
console.error(`Invalid spec file name: ${path}. You're using the old convention.`);
184+
});
185+
186+
// Run the tests.
187+
const allTests =
188+
glob.sync('packages/**/*_spec.ts')
189+
.map(p => relative(projectBaseDir, p))
190+
.filter(p => !/schematics_cli\/schematics\//.test(p));
191+
192+
export default function(_args: any) {
193+
runner.execute(allTests);
194+
}

0 commit comments

Comments
 (0)