UNPKG

8.95 kBJavaScriptView Raw
1import rimraf from 'rimraf';
2import cacache from 'cacache';
3import globalCacheDir from 'cachedir';
4import etag from 'etag';
5import execa from 'execa';
6import projectCacheDir from 'find-cache-dir';
7import findUp from 'find-up';
8import fs from 'fs';
9import got from 'got';
10import open from 'open';
11import path from 'path';
12import mkdirp from 'mkdirp';
13export const PIKA_CDN = `https://cdn.pika.dev`;
14export const GLOBAL_CACHE_DIR = globalCacheDir('snowpack');
15export const RESOURCE_CACHE = path.join(GLOBAL_CACHE_DIR, 'pkg-cache-1.4');
16export const BUILD_CACHE = path.join(GLOBAL_CACHE_DIR, 'build-cache-1.4');
17export const PROJECT_CACHE_DIR = projectCacheDir({ name: 'snowpack' });
18export const DEV_DEPENDENCIES_DIR = path.join(PROJECT_CACHE_DIR, 'dev');
19const LOCKFILE_HASH_FILE = '.hash';
20export const HAS_CDN_HASH_REGEX = /\-[a-zA-Z0-9]{16,}/;
21// NOTE(fks): Must match empty script elements to work properly.
22export const HTML_JS_REGEX = /(<script.*?>)(.*?)<\/script>/gms;
23export function isYarn(cwd) {
24 return fs.existsSync(path.join(cwd, 'yarn.lock'));
25}
26export async function readLockfile(cwd) {
27 try {
28 var lockfileContents = fs.readFileSync(path.join(cwd, 'snowpack.lock.json'), {
29 encoding: 'utf8',
30 });
31 }
32 catch (err) {
33 // no lockfile found, ignore and continue
34 return null;
35 }
36 // If this fails, we actually do want to alert the user by throwing
37 return JSON.parse(lockfileContents);
38}
39export async function writeLockfile(loc, importMap) {
40 const sortedImportMap = { imports: {} };
41 for (const key of Object.keys(importMap.imports).sort()) {
42 sortedImportMap.imports[key] = importMap.imports[key];
43 }
44 fs.writeFileSync(loc, JSON.stringify(sortedImportMap, undefined, 2), { encoding: 'utf8' });
45}
46export function fetchCDNResource(resourceUrl, responseType) {
47 if (!resourceUrl.startsWith(PIKA_CDN)) {
48 resourceUrl = PIKA_CDN + resourceUrl;
49 }
50 // @ts-ignore - TS doesn't like responseType being unknown amount three options
51 return got(resourceUrl, {
52 responseType: responseType,
53 headers: { 'user-agent': `snowpack/v1.4 (https://snowpack.dev)` },
54 throwHttpErrors: false,
55 });
56}
57export function isTruthy(item) {
58 return Boolean(item);
59}
60/** Get the package name + an entrypoint within that package (if given). */
61export function parsePackageImportSpecifier(imp) {
62 const impParts = imp.split('/');
63 if (imp.startsWith('@')) {
64 const [scope, name, ...rest] = impParts;
65 return [`${scope}/${name}`, rest.join('/') || null];
66 }
67 const [name, ...rest] = impParts;
68 return [name, rest.join('/') || null];
69}
70/**
71 * Given a package name, look for that package's package.json manifest.
72 * Return both the manifest location (if believed to exist) and the
73 * manifest itself (if found).
74 *
75 * NOTE: You used to be able to require() a package.json file directly,
76 * but now with export map support in Node v13 that's no longer possible.
77 */
78export function resolveDependencyManifest(dep, cwd) {
79 // Attempt #1: Resolve the dependency manifest normally. This works for most
80 // packages, but fails when the package defines an export map that doesn't
81 // include a package.json. If we detect that to be the reason for failure,
82 // move on to our custom implementation.
83 try {
84 const depManifest = require.resolve(`${dep}/package.json`, { paths: [cwd] });
85 return [depManifest, require(depManifest)];
86 }
87 catch (err) {
88 // if its an export map issue, move on to our manual resolver.
89 if (err.code !== 'ERR_PACKAGE_PATH_NOT_EXPORTED') {
90 return [null, null];
91 }
92 }
93 // Attempt #2: Resolve the dependency manifest manually. This involves resolving
94 // the dep itself to find the entrypoint file, and then haphazardly replacing the
95 // file path within the package with a "./package.json" instead. It's not as
96 // thorough as Attempt #1, but it should work well until export maps become more
97 // established & move out of experimental mode.
98 let result = [null, null];
99 try {
100 const fullPath = require.resolve(dep, { paths: [cwd] });
101 // Strip everything after the package name to get the package root path
102 // NOTE: This find-replace is very gross, replace with something like upath.
103 const searchPath = `${path.sep}node_modules${path.sep}${dep.replace('/', path.sep)}`;
104 const indexOfSearch = fullPath.lastIndexOf(searchPath);
105 if (indexOfSearch >= 0) {
106 const manifestPath = fullPath.substring(0, indexOfSearch + searchPath.length + 1) + 'package.json';
107 result[0] = manifestPath;
108 const manifestStr = fs.readFileSync(manifestPath, { encoding: 'utf8' });
109 result[1] = JSON.parse(manifestStr);
110 }
111 }
112 catch (err) {
113 // ignore
114 }
115 finally {
116 return result;
117 }
118}
119/**
120 * If Rollup erred parsing a particular file, show suggestions based on its
121 * file extension (note: lowercase is fine).
122 */
123export const MISSING_PLUGIN_SUGGESTIONS = {
124 '.svelte': 'Try installing rollup-plugin-svelte and adding it to Snowpack (https://www.snowpack.dev/#custom-rollup-plugins)',
125 '.vue': 'Try installing rollup-plugin-vue and adding it to Snowpack (https://www.snowpack.dev/#custom-rollup-plugins)',
126};
127const appNames = {
128 win32: {
129 brave: 'brave',
130 chrome: 'chrome',
131 },
132 darwin: {
133 brave: 'Brave Browser',
134 chrome: 'Google Chrome',
135 },
136 linux: {
137 brave: 'brave',
138 chrome: 'google-chrome',
139 },
140};
141export async function openInBrowser(protocol, port, browser) {
142 const url = `${protocol}//localhost:${port}`;
143 browser = /chrome/i.test(browser)
144 ? appNames[process.platform]['chrome']
145 : /brave/i.test(browser)
146 ? appNames[process.platform]['brave']
147 : browser;
148 if (process.platform === 'darwin' && /chrome|default/i.test(browser)) {
149 // If we're on macOS, and we haven't requested a specific browser,
150 // we can try opening Chrome with AppleScript. This lets us reuse an
151 // existing tab when possible instead of creating a new one.
152 try {
153 await execa.command('ps cax | grep "Google Chrome"', {
154 shell: true,
155 });
156 await execa('osascript ../assets/openChrome.applescript "' + encodeURI(url) + '"', {
157 cwd: __dirname,
158 stdio: 'ignore',
159 shell: true,
160 });
161 return true;
162 }
163 catch (err) {
164 // If macOS auto-reuse doesn't work, just open normally, using default browser.
165 open(url);
166 }
167 }
168 else {
169 browser === 'default' ? open(url) : open(url, { app: browser });
170 }
171}
172export async function checkLockfileHash(dir) {
173 const lockfileLoc = await findUp(['package-lock.json', 'yarn.lock']);
174 if (!lockfileLoc) {
175 return true;
176 }
177 const hashLoc = path.join(dir, LOCKFILE_HASH_FILE);
178 const newLockHash = etag(await fs.promises.readFile(lockfileLoc, 'utf-8'));
179 const oldLockHash = await fs.promises.readFile(hashLoc, 'utf-8').catch(() => '');
180 return newLockHash === oldLockHash;
181}
182export async function updateLockfileHash(dir) {
183 const lockfileLoc = await findUp(['package-lock.json', 'yarn.lock']);
184 if (!lockfileLoc) {
185 return;
186 }
187 const hashLoc = path.join(dir, LOCKFILE_HASH_FILE);
188 const newLockHash = etag(await fs.promises.readFile(lockfileLoc));
189 await mkdirp(path.dirname(hashLoc));
190 await fs.promises.writeFile(hashLoc, newLockHash);
191}
192export async function clearCache() {
193 return Promise.all([
194 cacache.rm.all(RESOURCE_CACHE),
195 cacache.rm.all(BUILD_CACHE),
196 rimraf.sync(PROJECT_CACHE_DIR),
197 ]);
198}
199/**
200 * Given an import string and a list of scripts, return the mount script that matches the import.
201 *
202 * `mount ./src --to /_dist_` and `mount src --to /_dist_` match `src/components/Button`
203 * `mount src --to /_dist_` does not match `package/components/Button`
204 */
205export function findMatchingMountScript(scripts, spec) {
206 // Only match bare module specifiers. relative and absolute imports should not match
207 if (spec.startsWith('./') ||
208 spec.startsWith('../') ||
209 spec.startsWith('/') ||
210 spec.startsWith('http://') ||
211 spec.startsWith('https://')) {
212 return null;
213 }
214 return scripts
215 .filter((script) => script.type === 'mount')
216 .find(({ args }) => spec.startsWith(args.fromDisk));
217}
218/** Get full extensions of files */
219export function getExt(fileName) {
220 return {
221 /** base extension (e.g. `.js`) */
222 baseExt: path.extname(fileName).toLocaleLowerCase(),
223 /** full extension, if applicable (e.g. `.proxy.js`) */
224 expandedExt: path.basename(fileName).replace(/[^.]+/, '').toLocaleLowerCase(),
225 };
226}