forked from angular/angular
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathexample-sandbox.mjs
183 lines (163 loc) · 7.66 KB
/
example-sandbox.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import jsonc from 'cjson';
import fs from 'fs-extra';
import {globbySync} from 'globby';
import path from 'node:path';
import os from 'node:os';
// Construct a sandbox environment for an example, linking in shared example node_modules
// and optionally linking in locally-built angular packages.
export async function constructExampleSandbox(examplePath, destPath, nodeModulesPath, localPackages) {
// If the sandbox folder exists delete the contents but not the folder itself. If cd'ed
// into the sandbox then bash will lose the reference and be stuck in a stray deleted folder,
// creating a confusing experience. This is relevant for developer experience with ibazel.
globbySync(`${destPath}/*`, {onlyFiles: false}).forEach(file => {
fs.rmSync(file, {
recursive: true,
force: true
});
})
fs.copySync(examplePath, destPath);
// Remove write protection as the example was copied from bazel output tree
chmodSyncRec(destPath);
// Symlink shared example node_modules, substituting for locally built deps if requested
await constructSymlinkedNodeModules(destPath, nodeModulesPath, localPackages);
// Add preserveSymlinks fixups to various files --- needed when linkin in local deps
ensurePreserveSymlinks(destPath);
}
async function constructSymlinkedNodeModules(examplePath, exampleDepsNodeModules, localPackages) {
const linkedNodeModules = path.resolve(examplePath, 'node_modules');
fs.ensureDirSync(linkedNodeModules);
await Promise.all([
linkExampleDeps(exampleDepsNodeModules, linkedNodeModules, localPackages),
linkLocalDeps(exampleDepsNodeModules, linkedNodeModules, localPackages)
]);
fs.copySync(path.join(exampleDepsNodeModules, '.bin'), path.join(linkedNodeModules, '.bin'));
pointBinSymlinksToLocalPackages(linkedNodeModules, exampleDepsNodeModules, localPackages);
}
function linkExampleDeps(exampleDepsNodeModules, linkedNodeModules, localPackages) {
const exampleDepsPackages = getPackageNamesFromNodeModules(exampleDepsNodeModules);
return Promise.all(exampleDepsPackages
.filter(pkgName => !(pkgName in localPackages))
.map(pkgName => fs.ensureSymlink(
path.join(exampleDepsNodeModules, pkgName),
path.join(linkedNodeModules, pkgName), 'dir'))
);
}
function getPackageNamesFromNodeModules(nodeModulesPath) {
return globbySync([
'@*/*',
'!@*$', // Exclude a namespace folder itself
'(?!@)*',
'!.bin',
'!.yarn-integrity',
'!_*'
], {
cwd: nodeModulesPath,
onlyDirectories: true,
dot: true
});
}
async function linkLocalDeps(exampleDepsNodeModules, linkedNodeModules, localPackages) {
const hasNpmDepForPkg = await Promise.all(Object.keys(localPackages)
.map(pkgName => fs.pathExists(path.join(exampleDepsNodeModules, pkgName))));
return Promise.all(Object.keys(localPackages).filter((pkgName, i) => hasNpmDepForPkg[i])
.map(pkgName => {
const pkgJsonPath = path.join(localPackages[pkgName], 'package.json');
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
const linkedPkgPath = path.join(linkedNodeModules, pkgName);
// If a local package has bin entries and an example invokes one, then entrypoint
// resolution escapes out of the sandboxed folder into the package folder in the output
// tree and the bin cannot resolve its deps. To prevent this, copy the full local package
// into node_modules as a way of linking it. Currently, only the `upgrade-phonecat-hybrid-2`
// which calls `ngc` seems to need this.
if (pkgJson.bin) {
fs.copySync(localPackages[pkgName], linkedPkgPath);
return;
}
// Standard case: just symlink the local package.
fs.ensureSymlinkSync(localPackages[pkgName], linkedPkgPath, 'dir')
}));
}
// The .bin folder is copied over from the original yarn_install repository, so the
// bin symlinks point there. When we link local packages in place of their npm equivalent,
// we need to alter those symlinks to point into the local package.
function pointBinSymlinksToLocalPackages(linkedNodeModules, exampleDepsNodeModules, localPackages) {
if (os.platform() === 'win32') {
// Bins on Windows are not symlinks; they are scripts that will invoke the bin
// relative to their location. The relative path will already point to the symlinked
// local package, so no further action is required.
return;
}
const allNodeModuleBins = globbySync(['**'], {
cwd: path.join(linkedNodeModules, '.bin'),
onlyFiles: true
});
allNodeModuleBins.forEach(bin => {
const symlinkTarget = fs.readlinkSync(path.join(linkedNodeModules, '.bin', bin));
for (const pkgName of Object.keys(localPackages)) {
const binMightBeInLocalPackage = symlinkTarget.includes(path.join(exampleDepsNodeModules, pkgName) + path
.sep);
if (binMightBeInLocalPackage) {
const pathToBinWithinPackage = symlinkTarget.substring(symlinkTarget.indexOf(pkgName) +
pkgName.length + path.sep.length);
const binExistsInLocalPackage = fs.existsSync(path.join(linkedNodeModules, pkgName,
pathToBinWithinPackage));
if (binExistsInLocalPackage) {
// Replace the copied bin symlink with one that points to the symlinked local package.
fs.rmSync(path.join(linkedNodeModules, '.bin', bin));
fs.ensureSymlinkSync(path.join(
'..',
pkgName,
pathToBinWithinPackage),
path.join(linkedNodeModules, '.bin', bin)
);
}
break;
}
}
});
}
/**
* When local packages are symlinked in, node will by default resolve local packages to
* their output location in the `bazel-bin`. This will then cause transitive dependencies
* to be incorrectly resolved from `bazel-bin`, instead of from within the example sandbox.
*
* Setting `preserveSymlinks` in relevant files fixes this. Note that we are intending to
* preserve symlinks in general (regardless of local packages being used), because it
* allows us to safely enable `NODE_PRESERVE_SYMLINKS=1` when executing commands inside.
*/
function ensurePreserveSymlinks(appDir) {
// Set preserveSymlinks in angular.json
const angularJsonPath = path.join(appDir, 'angular.json');
if (fs.existsSync(angularJsonPath)) {
const angularJson = jsonc.load(angularJsonPath, {encoding: 'utf-8'});
angularJson.projects['angular.io-example'].architect.build.options.preserveSymlinks = true;
angularJson.projects['angular.io-example'].architect.test.options.preserveSymlinks = true;
fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, undefined, 2));
}
// Set preserveSymlinks in any tsconfig.json files
const tsConfigPaths = globbySync([path.join(appDir, 'tsconfig*.json')]);
for (const tsConfigPath of tsConfigPaths) {
const tsConfig = jsonc.load(tsConfigPath, {encoding: 'utf-8'});
const isRootConfig = !tsConfig.extends;
if (isRootConfig) {
tsConfig.compilerOptions.preserveSymlinks = true;
fs.writeFileSync(tsConfigPath, JSON.stringify(tsConfig, undefined, 2));
}
}
// Call rollup with --preserveSymlinks
const packageJsonPath = path.join(appDir, 'package.json');
const packageJson = jsonc.load(packageJsonPath, {encoding: 'utf-8'});
if ('rollup' in packageJson.dependencies || 'rollup' in packageJson.devDependencies) {
packageJson.scripts.rollup = 'rollup --preserveSymlinks';
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, undefined, 2));
}
}
function chmodSyncRec(dest) {
// Glob patterns always use unix-style paths
const allFilesPattern = path.join(dest, '**').replace(/\\/g, '/');
globbySync(allFilesPattern, {
dot: true,
onlyFiles: false
}).forEach(file => fs.chmodSync(file, '755'));
fs.chmodSync(dest, '755');
}