Skip to content

Commit

Permalink
wip (#1161)
Browse files Browse the repository at this point in the history
  • Loading branch information
FredKSchott authored Oct 1, 2020
1 parent 5aef32a commit 7593243
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 142 deletions.
25 changes: 6 additions & 19 deletions esinstall/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ function isImportOfPackage(importUrl: string, packageName: string) {
*/
function resolveWebDependency(
dep: string,
{logger, cwd}: {logger: AbstractLogger; cwd: string},
{cwd}: {cwd: string},
): DependencyLoc {
// if dep points directly to a file within a package, return that reference.
// No other lookup required.
Expand All @@ -114,10 +114,9 @@ function resolveWebDependency(
exportMapEntry?.require ||
exportMapEntry;
if (typeof exportMapValue !== 'string') {
logger.error(
throw new Error(
`Package "${packageName}" exists but package.json "exports" does not include entry for "./${packageEntrypoint}".`,
);
process.exit(1);
}
return {
type: 'JS',
Expand Down Expand Up @@ -151,10 +150,9 @@ function resolveWebDependency(
depManifest.name &&
(depManifest.name.startsWith('@reactesm') || depManifest.name.startsWith('@pika/react'))
) {
logger.error(
throw new Error(
`React workaround packages no longer needed! Revert back to the official React & React-DOM packages.`,
);
process.exit(1);
}
let foundEntrypoint: string =
depManifest['browser:module'] ||
Expand Down Expand Up @@ -235,16 +233,9 @@ interface InstallOptions {
type PublicInstallOptions = Partial<InstallOptions>;
export {PublicInstallOptions as InstallOptions};

type InstallResult =
| {success: false; importMap: null; stats: null}
| {success: true; importMap: ImportMap; stats: DependencyStatsOutput};
type InstallResult = {importMap: ImportMap; stats: DependencyStatsOutput};

const FAILED_INSTALL_MESSAGE = 'Install failed.';
const EMPTY_INSTALL_RETURN: InstallResult = {
success: false,
importMap: null,
stats: null,
};

function setOptionDefaults(_options: PublicInstallOptions): InstallOptions {
const options = {
Expand Down Expand Up @@ -319,7 +310,6 @@ export async function install(
}
try {
const resolvedResult = resolveWebDependency(installSpecifier, {
logger,
cwd,
});
if (resolvedResult.type === 'JS') {
Expand All @@ -341,18 +331,16 @@ export async function install(
if (skipFailures) {
continue;
}
logger.error(err.message || err);
throw new Error(FAILED_INSTALL_MESSAGE);
throw err;
}
}
if (Object.keys(installEntrypoints).length === 0 && Object.keys(assetEntrypoints).length === 0) {
logger.error(`No ESM dependencies found!
throw new Error(`No ESM dependencies found!
${colors.dim(
` At least one dependency must have an ESM "module" entrypoint. You can find modern, web-ready packages at ${colors.underline(
'https://www.pika.dev',
)}`,
)}`);
return EMPTY_INSTALL_RETURN;
}

await initESModuleLexer;
Expand Down Expand Up @@ -485,7 +473,6 @@ ${colors.dim(
}

return {
success: true,
importMap,
stats: dependencyStats!,
};
Expand Down
5 changes: 2 additions & 3 deletions snowpack/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ async function installOptimizedDependencies(
...commandOptions,
installTargets,
config: installConfig,
shouldPrintStats: true,
shouldWriteLockfile: false,
});
return installResult;
}
Expand Down Expand Up @@ -332,9 +334,6 @@ export async function command(commandOptions: CommandOptions) {
const installResult = await installOptimizedDependencies(scannedFiles, installDest, {
...commandOptions,
});
if (!installResult.success || installResult.hasError || !installResult.importMap) {
process.exit(1);
}
const allFiles = glob.sync(`**/*`, {
cwd: installDest,
absolute: true,
Expand Down
107 changes: 82 additions & 25 deletions snowpack/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ import {
resolveDependencyManifest,
updateLockfileHash,
} from '../util';
import {command as installCommand} from './install';
import {getInstallTargets, run as installRunner} from './install';
import {getPort, paint, paintEvent} from './paint';

const DEFAULT_PROXY_ERROR_HANDLER = (
Expand Down Expand Up @@ -129,6 +129,30 @@ class InMemoryBuildCache {
}
}

/**
* Install dependencies needed in "dev" mode. Generally speaking, this scans
* your entire source app for dependency install targets, installs them,
* and then updates the "hash" file used to check node_modules freshness.
*/
async function installDependencies(commandOptions: CommandOptions) {
const {config} = commandOptions;
const installTargets = await getInstallTargets(config);
if (installTargets.length === 0) {
logger.info('Nothing to install.');
return;
}
// 2. Install dependencies, based on the scan of your final build.
const installResult = await installRunner({
...commandOptions,
installTargets,
config,
shouldPrintStats: true,
shouldWriteLockfile: false,
});
await updateLockfileHash(DEV_DEPENDENCIES_DIR);
return installResult;
}

function shouldProxy(pathPrefix: string, req: http.IncomingMessage) {
const reqPath = decodeURI(url.parse(req.url!).pathname!);
return reqPath.startsWith(pathPrefix);
Expand Down Expand Up @@ -270,15 +294,6 @@ export async function startServer(commandOptions: CommandOptions) {
});

// Start with a fresh install of your dependencies, if needed.
if (!(await checkLockfileHash(DEV_DEPENDENCIES_DIR)) || !existsSync(dependencyImportMapLoc)) {
logger.debug('Cache out of date or missing. Updating…');
logger.info(colors.yellow('! updating dependencies...'));
await installCommand(installCommandOptions);
await updateLockfileHash(DEV_DEPENDENCIES_DIR);
} else {
logger.debug(`Cache up-to-date. Using existing cache`);
}

let dependencyImportMap: ImportMap = {imports: {}};
try {
dependencyImportMap = JSON.parse(
Expand All @@ -288,6 +303,14 @@ export async function startServer(commandOptions: CommandOptions) {
// no import-map found, safe to ignore
}

if (!(await checkLockfileHash(DEV_DEPENDENCIES_DIR)) || !existsSync(dependencyImportMapLoc)) {
logger.debug('Cache out of date or missing. Updating...');
const installResult = await installDependencies(installCommandOptions);
dependencyImportMap = installResult?.importMap || dependencyImportMap;
} else {
logger.debug(`Cache up-to-date. Using existing cache`);
}

const devProxies = {};
config.proxy.forEach(([pathPrefix, proxyOptions]) => {
const proxyServer = (devProxies[pathPrefix] = HttpProxy.createProxyServer(proxyOptions));
Expand Down Expand Up @@ -582,7 +605,9 @@ export async function startServer(commandOptions: CommandOptions) {
fileLoc: string,
responseExt: string,
wrappedResponse: string,
retryMissing = true,
): Promise<string> {
let missingPackages: string[] = [];
const resolveImportSpecifier = createImportResolver({
fileLoc,
dependencyImportMap,
Expand Down Expand Up @@ -614,25 +639,51 @@ export async function startServer(commandOptions: CommandOptions) {
}
return resolvedImportUrl;
}
const errorTitle = `Error: Import "${spec}" could not be resolved.`;
const errorMessage = `If this is a new package, re-run Snowpack with the ${colors.bold(
'--reload',
)} flag to rebuild.
If Snowpack is having trouble detecting the import, add ${colors.bold(
`"install": ["${spec}"]`,
)} to your Snowpack config file.`;
logger.error(`${errorTitle}\n${errorMessage}`);
hmrEngine.broadcastMessage({
type: 'error',
title: `${errorTitle}`,
errorMessage,
fileLoc,
});

missingPackages.push(spec);
return spec;
},
);

// A missing package is a broken import, so we need to recover instantly if possible.
if (missingPackages.length > 0) {
// if retryMissing is true, do a fresh dependency install and then retry.
// Only retry once, to prevent an infinite loop when a package doesn't actually exist.
if (retryMissing) {
try {
logger.info(colors.yellow('Dependency cache out of date. Updating...'));
const installResult = await installDependencies(installCommandOptions);
dependencyImportMap = installResult?.importMap || dependencyImportMap;
return resolveResponseImports(fileLoc, responseExt, wrappedResponse, false);
} catch (err) {
const errorTitle = `Dependency Install Error`;
const errorMessage = err.message;
logger.error(`${errorTitle}: ${errorMessage}`);
hmrEngine.broadcastMessage({
type: 'error',
title: errorTitle,
errorMessage,
fileLoc,
});
return wrappedResponse;
}
}
// Otherwise, we need to send an error to the user, telling them about this issue.
// A failed retry usually means that Snowpack couldn't detect the import that the browser
// eventually saw post-build. In that case, you need to add it manually.
const errorTitle = `Error: Import "${missingPackages[0]}" could not be resolved.`;
const errorMessage = `If this import doesn't exist in the source file, add ${colors.bold(
`"install": ["${missingPackages[0]}"]`,
)} to your Snowpack config file.`;
logger.error(`${errorTitle}\n${errorMessage}`);
hmrEngine.broadcastMessage({
type: 'error',
title: errorTitle,
errorMessage,
fileLoc,
});
}

let code = wrappedResponse;
if (responseFileExt === '.js' && reqUrlHmrParam)
code = await transformEsmImports(code as string, (imp) => {
Expand Down Expand Up @@ -1032,6 +1083,12 @@ ${err}`);
}

export async function command(commandOptions: CommandOptions) {
await startServer(commandOptions);
try {
await startServer(commandOptions);
} catch (err) {
logger.error(err.message);
logger.debug(err.stack);
process.exit(1);
}
return new Promise(() => {});
}
63 changes: 32 additions & 31 deletions snowpack/src/commands/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function getInstallTargets(
}

export async function command(commandOptions: CommandOptions) {
const {cwd, config} = commandOptions;
const {config} = commandOptions;

logger.debug('Starting install');
const installTargets = await getInstallTargets(config);
Expand All @@ -43,29 +43,31 @@ export async function command(commandOptions: CommandOptions) {
return;
}
logger.debug('Running install command');
const finalResult = await run({...commandOptions, installTargets});
logger.debug('Install command successfully ran');
if (finalResult.newLockfile) {
await writeLockfile(path.join(cwd, 'snowpack.lock.json'), finalResult.newLockfile);
logger.debug('Successfully wrote lockfile');
}
if (finalResult.stats) {
logger.info(printStats(finalResult.stats));
}

if (!finalResult.success || finalResult.hasError) {
await run({
...commandOptions,
installTargets,
shouldPrintStats: true,
shouldWriteLockfile: true,
}).catch((err) => {
if (err.loc) {
logger.error(colors.red(colors.bold(`✘ ${err.loc.file}`)));
}
if (err.url) {
logger.error(colors.dim(`👉 ${err.url}`));
}
logger.error(err.message || err);
process.exit(1);
}
});
}

interface InstallRunOptions extends CommandOptions {
installTargets: InstallTarget[];
shouldWriteLockfile: boolean;
shouldPrintStats: boolean;
}

interface InstallRunResult {
success: boolean;
hasError: boolean;
importMap: ImportMap | null;
importMap: ImportMap;
newLockfile: ImportMap | null;
stats: DependencyStatsOutput | null;
}
Expand All @@ -74,17 +76,17 @@ export async function run({
config,
lockfile,
installTargets,
shouldWriteLockfile,
shouldPrintStats,
}: InstallRunOptions): Promise<InstallRunResult> {
const {webDependencies} = config;

// start
const installStart = performance.now();
logger.info(colors.yellow('! installing dependencies'));
logger.info(colors.yellow('installing dependencies...'));

if (installTargets.length === 0) {
return {
success: true,
hasError: false,
importMap: {imports: {}} as ImportMap,
newLockfile: null,
stats: null,
Expand All @@ -110,31 +112,30 @@ export async function run({
error: (...args: [any, ...any[]]) => logger.error(util.format(...args)),
},
...config.installOptions,
}).catch((err) => {
if (err.loc) {
logger.error(colors.red(colors.bold(`✘ ${err.loc.file}`)));
}
if (err.url) {
logger.error(colors.dim(`👉 ${err.url}`));
}
logger.error(err.message || err);
process.exit(1);
});

logger.debug('Install ran successfully!');
if (shouldWriteLockfile && newLockfile) {
await writeLockfile(path.join(cwd, 'snowpack.lock.json'), newLockfile);
logger.debug('Successfully wrote lockfile');
}

// finish
const installEnd = performance.now();
const depList = (finalResult.importMap && Object.keys(finalResult.importMap.imports)) || [];
logger.info(
`${
depList.length
? colors.green(`✔`) + ' install complete'
? colors.green(`✔`) + ' install complete!'
: 'install skipped (nothing to install)'
} ${colors.dim(`[${((installEnd - installStart) / 1000).toFixed(2)}s]`)}`,
);

if (shouldPrintStats && finalResult.stats) {
logger.info(printStats(finalResult.stats));
}

return {
success: true,
hasError: false,
importMap: finalResult.importMap,
newLockfile,
stats: finalResult.stats!,
Expand Down
Loading

1 comment on commit 7593243

@vercel
Copy link

@vercel vercel bot commented on 7593243 Oct 1, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.