UNPKG

11.6 kBJavaScriptView Raw
1const { resolve } = require('path');
2
3const uniqBy = (array, property) => {
4 const map = new Map();
5
6 for (const element of array) {
7 if (element && element.hasOwnProperty(property)) {
8 map.set(element[property], element);
9 }
10 }
11
12 return Array.from(map.values());
13};
14
15const removeDuplicateBackends = backendEnvironments =>
16 uniqBy(backendEnvironments, 'url');
17
18const fetchSampleBackends = async defaultSampleBackends => {
19 try {
20 const res = await fetch(
21 'https://fvp0esmt8f.execute-api.us-east-1.amazonaws.com/default/getSampleBackends'
22 );
23 const { sampleBackends } = await res.json();
24
25 return removeDuplicateBackends([
26 ...sampleBackends.environments,
27 ...defaultSampleBackends.environments
28 ]).map(({ url }) => url);
29 } catch {
30 return defaultSampleBackends.environments.map(({ url }) => url);
31 }
32};
33
34async function createProjectFromVenia({ fs, tasks, options, sampleBackends }) {
35 const npmCli = options.npmClient;
36 const sampleBackendEnvironments = await fetchSampleBackends(sampleBackends);
37
38 const toCopyFromPackageJson = [
39 'main',
40 'browser',
41 'dependencies',
42 'devDependencies',
43 'optionalDependencies',
44 'resolutions',
45 'engines',
46 'pwa-studio'
47 ];
48 const scriptsToCopy = [
49 'buildpack',
50 'build',
51 'build:analyze',
52 'build:dev',
53 'build:prod',
54 'build:report',
55 'clean',
56 'lint',
57 'prettier',
58 'prettier:check',
59 'prettier:fix',
60 'start',
61 'start:debug',
62 'watch'
63 ];
64 const scriptsToInsert = {
65 storybook: 'start-storybook -p 9001 -c src/.storybook',
66 'storybook:build': 'build-storybook -c src/.storybook -o storybook-dist'
67 };
68
69 const filesToIgnore = [
70 'CHANGELOG*',
71 'LICENSE*',
72 '_buildpack',
73 '_buildpack/**',
74 // These tests are teporarily removed until we can implement a test
75 // harness for the scaffolded app. See PWA-508.
76 '**/__tests__',
77 '**/__tests__/**'
78 ];
79 const ignoresGlob = `{${filesToIgnore.join(',')}}`;
80
81 return {
82 after({ options }) {
83 // The venia-concept directory doesn't have its own babel.config.js
84 // since that would interfere with monorepo configuration.
85 // Therefore there is nothing to copy, so we use the "after" event
86 // to write that file directly.
87 fs.outputFileSync(
88 resolve(options.directory, 'babel.config.js'),
89 "module.exports = { presets: ['@magento/peregrine'] };\n",
90 'utf8'
91 );
92 },
93 visitor: {
94 // Modify package.json with user details before copying it.
95 'package.json': ({
96 path,
97 targetPath,
98 options: { name, author, backendUrl }
99 }) => {
100 const pkgTpt = fs.readJsonSync(path);
101 const pkg = {
102 name,
103 private: true,
104 version: '0.0.1',
105 description:
106 'A new project based on @magento/venia-concept',
107 author,
108 license: 'UNLICENSED',
109 scripts: {}
110 };
111 toCopyFromPackageJson.forEach(prop => {
112 pkg[prop] = pkgTpt[prop];
113 });
114
115 // If the backend url is a sample backend, add the validator.
116 if (sampleBackendEnvironments.includes(backendUrl)) {
117 pkg.devDependencies = {
118 ...pkg.devDependencies,
119 '@magento/venia-sample-backends': '~0.0.1'
120 };
121 }
122
123 // The venia-concept template is part of the monorepo, which
124 // uses yarn for workspaces. But if the user wants to use
125 // npm, then the scripts which use `yarn` must change.
126 const toPackageScript = script => {
127 const outputScript = script.replace(/\bvenia\b/g, name);
128 return npmCli === 'npm'
129 ? outputScript.replace(/yarn run/g, 'npm run')
130 : outputScript;
131 };
132
133 if (!pkgTpt.scripts) {
134 throw new Error(
135 JSON.stringify(pkgTpt, null, 2) +
136 '\ndoes not have a "scripts"'
137 );
138 }
139 scriptsToCopy.forEach(name => {
140 if (pkgTpt.scripts[name]) {
141 pkg.scripts[name] = toPackageScript(
142 pkgTpt.scripts[name]
143 );
144 }
145 });
146 Object.keys(scriptsToInsert).forEach(name => {
147 pkg.scripts[name] = toPackageScript(scriptsToInsert[name]);
148 });
149
150 if (process.env.DEBUG_PROJECT_CREATION) {
151 setDebugDependencies(pkg, fs);
152 }
153
154 fs.outputJsonSync(targetPath, pkg, {
155 spaces: 2
156 });
157 },
158 '.graphqlconfig': ({ path, targetPath, options: { name } }) => {
159 const config = fs.readJsonSync(path);
160 config.projects[name] = config.projects.venia;
161 delete config.projects.venia;
162 fs.outputJsonSync(targetPath, config, { spaces: 2 });
163 },
164 // These tasks are sequential so we must ignore before we copy.
165 [ignoresGlob]: tasks.IGNORE,
166 '**/*': tasks.COPY
167 }
168 };
169}
170
171function setDebugDependencies(pkg, fs) {
172 console.warn(
173 'DEBUG_PROJECT_CREATION: Debugging Venia _buildpack/create.js, so we will assume we are inside the pwa-studio repo and replace those package dependency declarations with local file paths.'
174 );
175
176 const { execSync } = require('child_process');
177 const overridden = {};
178 const monorepoDir = resolve(__dirname, '../../../');
179
180 // The Yarn "workspaces info" command outputs JSON as of v1.22.4.
181 // The -s flag suppresses all other non-JSON logging output.
182 const yarnWorkspaceInfoCmd = 'yarn -s workspaces info';
183 const workspaceInfo = execSync(yarnWorkspaceInfoCmd, { cwd: monorepoDir });
184
185 let packageDirs;
186 try {
187 // Build a list of package name => absolute package path tuples.
188 packageDirs = Object.entries(JSON.parse(workspaceInfo)).map(
189 ([name, info]) => [name, resolve(monorepoDir, info.location)]
190 );
191 } catch (e) {
192 throw new Error(
193 `DEBUG_PROJECT_CREATION: Could not parse output of '${yarnWorkspaceInfoCmd}:\n${workspaceInfo}. Please check your version of yarn is v1.22.4+.\n${
194 e.stack
195 }`
196 );
197 }
198
199 // Packages not found in the template that must also be locally packed
200 const transitivePackages = new Set([
201 '@magento/pwa-buildpack',
202 '@magento/upward-js'
203 ]);
204
205 // We'll look for existing dependencies in all of the dep collections that
206 // the package has.
207 const depTypes = [
208 'dependencies',
209 'devDependencies',
210 'optionalDependencies'
211 ].filter(type => pkg.hasOwnProperty(type));
212
213 const getNewestTarballIn = dir => {
214 const tarballsInDir = fs
215 .readdirSync(dir)
216 .filter(filename => filename.endsWith('.tgz'));
217 if (tarballsInDir.length === 0) {
218 throw new Error('Found no new .tgz files in ${dir}.');
219 }
220 // turn filename into a tuple of filename and modified time
221 const tarballsWithModifiedTime = tarballsInDir.map(filename => ({
222 filename,
223 modified: fs.statSync(resolve(dir, filename)).mtime
224 }));
225 // find the newest one (no need to sort, we only want the newest)
226 return tarballsWithModifiedTime.reduce((newest, candidate) =>
227 candidate.modified > newest.modified ? candidate : newest
228 ).filename;
229 };
230
231 // Modify the new project's package.json file to use our generated local
232 // dependencies instead of going to the NPM registry and getting the old
233 // versions of packages that haven't yet been released.
234 for (const [name, packageDir] of packageDirs) {
235 // skip packages not in the template that are also not transitive
236 if (
237 !depTypes.find(type => pkg[type].hasOwnProperty(name)) &&
238 !transitivePackages.has(name)
239 ) {
240 continue;
241 }
242
243 console.warn(`DEBUG_PROJECT_CREATION: Packing ${name} for local usage`);
244
245 // We want to use local versions of these packages, which normally would
246 // just be a `yarn link`. But symlinks and direct file URL pointers
247 // aren't reliable in this case, because of the monorepo structure.
248 // So instead, we use `npm pack` to make a tarball of each dependency,
249 // which the scaffolded project will unzip and install.
250 //
251 // ADDENDUM 2021-09-14:
252 // NPM 7 has a bug where the JSON output "filename" is wrong. It says
253 // "@magento/package-name-X.X.X.tgz" when the actual filename is
254 // "magento-package-name-X.X.X.tgz". The most reliable way to find the
255 // newly generated tarball is to scan packageDir for new tarball files.
256 let filename;
257 let packOutput;
258 try {
259 packOutput = execSync('npm pack -s --ignore-scripts', {
260 cwd: packageDir
261 });
262 filename = getNewestTarballIn(packageDir);
263 } catch (e) {
264 throw new Error(
265 `DEBUG_PROJECT_CREATION: npm pack in ${name} package failed: output was ${packOutput}\n\nerror was ${
266 e.message
267 }`
268 );
269 }
270
271 // The `file://` URL scheme is legal in package.json.
272 // https://docs.npmjs.com/files/package.json#urls-as-dependencies
273 const localDep = `file://${resolve(packageDir, filename)}`;
274
275 // All the local packages go in `overrides`, which we assign to
276 // package.json `resolutions` a little bit under here.
277 overridden[name] = localDep;
278
279 // If the project has an explicit dependency on this package already...
280 const depType =
281 depTypes.find(type => pkg[type].hasOwnProperty(name)) ||
282 // then depType will be the dependency collection where it was found.
283 // This way we replace an existing dependency and avoid duplicates.
284 // OR...
285 // If not, then put it in the dependencies collection anyway.
286 // That way, it can override any transitive dependencies that would
287 // pull in the old version.
288 'dependencies';
289
290 pkg[depType][name] = localDep;
291 }
292
293 if (Object.keys(overridden).length > 0) {
294 console.warn(
295 'DEBUG_PROJECT_CREATION: Resolved the following packages via local tarball',
296 JSON.stringify(overridden, null, 2)
297 );
298
299 // Force yarn to resolve all dependencies on these modules to the local
300 // versions we just created:
301 // https://classic.yarnpkg.com/en/docs/selective-version-resolutions/
302 pkg.resolutions = Object.assign({}, pkg.resolutions, overridden);
303 }
304}
305
306module.exports = createProjectFromVenia;