diff --git a/src/cli/task-test.ts b/src/cli/task-test.ts index 589629e703e..8ba346f40a5 100644 --- a/src/cli/task-test.ts +++ b/src/cli/task-test.ts @@ -36,10 +36,11 @@ export const taskTest = async (config: Config) => { } // ensure we've got the required modules installed - // jest and puppeteer are quite large, so this - // is an experiment to lazy install these - // modules only when you need them - await config.sys.lazyRequire.ensure(config.logger, config.rootDir, ensureModuleIds); + const diagnostics = await config.sys.lazyRequire.ensure(config.rootDir, ensureModuleIds); + if (diagnostics.length > 0) { + config.logger.printDiagnostics(diagnostics); + return config.sys.exit(1); + } // let's test! const { createTesting } = await import('@stencil/core/testing'); diff --git a/src/compiler/build/compiler-ctx.ts b/src/compiler/build/compiler-ctx.ts index c1fa84dbce9..d193cd73b02 100644 --- a/src/compiler/build/compiler-ctx.ts +++ b/src/compiler/build/compiler-ctx.ts @@ -1,7 +1,7 @@ import type * as d from '../../declarations'; import { basename, dirname, extname, join } from 'path'; import { buildEvents } from '../events'; -import { normalizePath } from '@utils'; +import { noop, normalizePath } from '@utils'; /** * The CompilerCtx is a persistent object that's reused throughout @@ -17,33 +17,28 @@ export class CompilerContext implements d.CompilerCtx { activeFilesUpdated: string[] = []; activeDirsAdded: string[] = []; activeDirsDeleted: string[] = []; + addWatchDir: (path: string) => void = noop; + addWatchFile: (path: string) => void = noop; cache: d.Cache; cachedStyleMeta = new Map(); + changedFiles = new Set(); + changedModules = new Set(); collections: d.CollectionCompilerMeta[] = []; compilerOptions: any = null; events = buildEvents(); fs: d.InMemoryFileSystem; - fsWatcher: d.FsWatcher = null; - hasFsWatcherEvents = false; - hasLoggedServerUrl = false; hasSuccessfulBuild = false; isActivelyBuilding = false; lastBuildResults: d.CompilerBuildResults = null; - lastBuildStyles = new Map(); - lastComponentStyleInput = new Map(); moduleMap: d.ModuleMap = new Map(); nodeMap = new WeakMap(); resolvedCollections = new Set(); + rollupCache = new Map(); rollupCacheHydrate: any = null; rollupCacheLazy: any = null; rollupCacheNative: any = null; - rootTsFiles: string[] = []; - tsService: d.TsService = null; cachedGlobalStyle: string; styleModeNames = new Set(); - rollupCache = new Map(); - changedModules = new Set(); - changedFiles = new Set(); worker: d.CompilerWorkerContext = null; reset() { @@ -52,14 +47,12 @@ export class CompilerContext implements d.CompilerCtx { this.cachedGlobalStyle = null; this.collections.length = 0; this.compilerOptions = null; - this.lastComponentStyleInput.clear(); + this.hasSuccessfulBuild = false; this.rollupCacheHydrate = null; this.rollupCacheLazy = null; this.rollupCacheNative = null; this.moduleMap.clear(); this.resolvedCollections.clear(); - this.rootTsFiles.length = 0; - this.tsService = null; if (this.fs != null) { this.fs.clearCache(); diff --git a/src/compiler/build/watch-build.ts b/src/compiler/build/watch-build.ts index 64254549d58..76fa3197bd9 100644 --- a/src/compiler/build/watch-build.ts +++ b/src/compiler/build/watch-build.ts @@ -4,16 +4,24 @@ import { BuildContext } from './build-ctx'; import { compilerRequest } from '../bundle/dev-module'; import { createTsWatchProgram } from '../transpile/create-watch-program'; import { dirname, resolve } from 'path'; -import { filesChanged, hasHtmlChanges, hasScriptChanges, hasStyleChanges, scriptsAdded, scriptsDeleted } from '../fs-watch/fs-watch-rebuild'; +import { + filesChanged, + hasHtmlChanges, + hasScriptChanges, + hasStyleChanges, + scriptsAdded, + scriptsDeleted, +} from '../fs-watch/fs-watch-rebuild'; import { hasServiceWorkerChanges } from '../service-worker/generate-sw'; +import { isString } from '@utils'; import type ts from 'typescript'; export const createWatchBuild = async (config: d.Config, compilerCtx: d.CompilerCtx): Promise => { let isRebuild = false; - let tsWatchProgram: { program: ts.WatchOfConfigFile; rebuild: () => void }; - let srcFileWatchCloser: () => void; - let otherFileWatchCloser: () => void; - + let tsWatchProgram: { + program: ts.WatchOfConfigFile; + rebuild: () => void; + }; let closeResolver: Function; const watchWaiter = new Promise(resolve => (closeResolver = resolve)); @@ -23,31 +31,6 @@ export const createWatchBuild = async (config: d.Config, compilerCtx: d.Compiler const filesUpdated = new Set(); const filesDeleted = new Set(); - const onSrcFileChange: d.CompilerFileWatcherCallback = (p, eventKind) => { - updateCompilerCtxCache(config, compilerCtx, p, eventKind); - - switch (eventKind) { - case 'dirAdd': - dirsAdded.add(p); - break; - case 'dirDelete': - dirsDeleted.add(p); - break; - case 'fileAdd': - filesAdded.add(p); - break; - case 'fileUpdate': - filesUpdated.add(p); - break; - case 'fileDelete': - filesDeleted.add(p); - break; - } - - config.logger.debug(`${eventKind}: ${p}`); - tsWatchProgram.rebuild(); - }; - const onBuild = async (tsBuilder: ts.BuilderProgram) => { const buildCtx = new BuildContext(config, compilerCtx); buildCtx.isRebuild = isRebuild; @@ -83,27 +66,61 @@ export const createWatchBuild = async (config: d.Config, compilerCtx: d.Compiler }; const start = async () => { - const srcRead = watchSrcDirectory(config, compilerCtx, onSrcFileChange); - const otherRead = watchOtherFiles(config, compilerCtx); - srcFileWatchCloser = await srcRead; - otherFileWatchCloser = await otherRead; + const srcRead = watchSrcDirectory(config, compilerCtx); + const otherRead = watchRootFiles(config, compilerCtx); + await srcRead; + await otherRead; tsWatchProgram = await createTsWatchProgram(config, onBuild); return watchWaiter; }; - const close = async () => { - if (srcFileWatchCloser) { - srcFileWatchCloser(); + const watchingDirs = new Map(); + const watchingFiles = new Map(); + + const onFsChange: d.CompilerFileWatcherCallback = (p, eventKind) => { + if (tsWatchProgram) { + updateCompilerCtxCache(config, compilerCtx, p, eventKind); + + switch (eventKind) { + case 'dirAdd': + dirsAdded.add(p); + break; + case 'dirDelete': + dirsDeleted.add(p); + break; + case 'fileAdd': + filesAdded.add(p); + break; + case 'fileUpdate': + filesUpdated.add(p); + break; + case 'fileDelete': + filesDeleted.add(p); + break; + } + + config.logger.debug(`onFsChange ${eventKind}: ${p}`); + tsWatchProgram.rebuild(); } - if (otherFileWatchCloser) { - otherFileWatchCloser(); + }; + + const onDirChange: d.CompilerFileWatcherCallback = (p, eventKind) => { + if (eventKind != null) { + onFsChange(p, eventKind); } + }; + + const close = async () => { + watchingDirs.forEach(w => w.close()); + watchingFiles.forEach(w => w.close()); + watchingDirs.clear(); + watchingFiles.clear(); + if (tsWatchProgram) { tsWatchProgram.program.close(); + tsWatchProgram = null; } - srcFileWatchCloser = otherFileWatchCloser = tsWatchProgram = null; - const watcherCloseResults: d.WatcherCloseResults = { exitCode: 0, }; @@ -113,6 +130,18 @@ export const createWatchBuild = async (config: d.Config, compilerCtx: d.Compiler const request = async (data: d.CompilerRequest) => compilerRequest(config, compilerCtx, data); + compilerCtx.addWatchFile = filePath => { + if (isString(filePath) && !watchingFiles.has(filePath)) { + watchingFiles.set(filePath, config.sys.watchFile(filePath, onFsChange)); + } + }; + + compilerCtx.addWatchDir = (dirPath, recursive) => { + if (isString(dirPath) && !watchingDirs.has(dirPath)) { + watchingDirs.set(dirPath, config.sys.watchDirectory(dirPath, onDirChange, recursive)); + } + }; + config.sys.addDestory(close); return { @@ -123,90 +152,47 @@ export const createWatchBuild = async (config: d.Config, compilerCtx: d.Compiler }; }; -const watchSrcDirectory = async (config: d.Config, compilerCtx: d.CompilerCtx, callback: d.CompilerFileWatcherCallback) => { - const watching = new Map(); - const watchFile = (path: string) => { - if (!watching.has(path)) { - watching.set(path, config.sys.watchFile(path, callback)); - } - }; - +const watchSrcDirectory = async (config: d.Config, compilerCtx: d.CompilerCtx) => { const srcFiles = await compilerCtx.fs.readdir(config.srcDir, { recursive: true, excludeDirNames: ['.cache', '.git', '.github', '.stencil', '.vscode', 'node_modules'], - excludeExtensions: ['.md', '.markdown', '.txt', '.spec.ts', '.spec.tsx', '.e2e.ts', '.e2e.tsx', '.gitignore', '.editorconfig'], + excludeExtensions: [ + '.md', + '.markdown', + '.txt', + '.spec.ts', + '.spec.tsx', + '.e2e.ts', + '.e2e.tsx', + '.gitignore', + '.editorconfig', + ], }); - srcFiles.filter(({ isFile }) => isFile).forEach(({ absPath }) => watchFile(absPath)); - - watching.set( - config.srcDir, - config.sys.watchDirectory(config.srcDir, (filename, kind) => { - if (kind != null) { - watchFile(filename); - callback(filename, kind); - } - }), - ); + srcFiles.filter(({ isFile }) => isFile).forEach(({ absPath }) => compilerCtx.addWatchFile(absPath)); - return () => { - watching.forEach(w => w.close()); - }; + compilerCtx.addWatchDir(config.srcDir, true); }; -const watchOtherFiles = async (config: d.Config, compilerCtx: d.CompilerCtx) => { +const watchRootFiles = async (config: d.Config, compilerCtx: d.CompilerCtx) => { // non-src files that cause a rebuild // mainly for root level config files, and getting an event when they change - const onFileChange: d.CompilerFileWatcherCallback = (p, eventKind) => { - const data: d.FsWatchResults = { - dirsAdded: [], - dirsDeleted: [], - filesUpdated: [], - filesAdded: [], - filesDeleted: [], - }; - - switch (eventKind) { - case 'dirAdd': - data.dirsAdded.push(p); - break; - case 'dirDelete': - data.dirsDeleted.push(p); - break; - case 'fileAdd': - data.filesAdded.push(p); - break; - case 'fileUpdate': - data.filesUpdated.push(p); - break; - case 'fileDelete': - data.filesDeleted.push(p); - break; - } - - compilerCtx.events.emit('fsChange', data); - }; - const watching = new Map(); - const watchFile = (path: string) => { - if (!watching.has(path)) { - watching.set(path, config.sys.watchFile(path, onFileChange)); - } - }; - const rootFiles = await compilerCtx.fs.readdir(config.rootDir, { recursive: false, excludeDirNames: ['.cache', '.git', '.github', '.stencil', '.vscode', 'node_modules'], }); - rootFiles.filter(({ isFile }) => isFile).forEach(({ absPath }) => watchFile(absPath)); - - return () => { - watching.forEach(w => w.close()); - }; + rootFiles.filter(({ isFile }) => isFile).forEach(({ absPath }) => compilerCtx.addWatchFile(absPath)); }; const emitFsChange = (compilerCtx: d.CompilerCtx, buildCtx: BuildContext) => { - if (buildCtx.dirsAdded.length > 0 || buildCtx.dirsDeleted.length > 0 || buildCtx.filesUpdated.length > 0 || buildCtx.filesAdded.length > 0 || buildCtx.filesDeleted.length > 0) { + if ( + buildCtx.dirsAdded.length > 0 || + buildCtx.dirsDeleted.length > 0 || + buildCtx.filesUpdated.length > 0 || + buildCtx.filesAdded.length > 0 || + buildCtx.filesDeleted.length > 0 + ) { compilerCtx.events.emit('fsChange', { dirsAdded: buildCtx.dirsAdded.slice(), dirsDeleted: buildCtx.dirsDeleted.slice(), @@ -217,7 +203,12 @@ const emitFsChange = (compilerCtx: d.CompilerCtx, buildCtx: BuildContext) => { } }; -const updateCompilerCtxCache = (config: d.Config, compilerCtx: d.CompilerCtx, path: string, kind: d.CompilerFileWatcherEvent) => { +const updateCompilerCtxCache = ( + config: d.Config, + compilerCtx: d.CompilerCtx, + path: string, + kind: d.CompilerFileWatcherEvent, +) => { compilerCtx.fs.clearFileCache(path); compilerCtx.changedFiles.add(path); diff --git a/src/compiler/bundle/ext-transforms-plugin.ts b/src/compiler/bundle/ext-transforms-plugin.ts index 5f99d8f351e..020e5681830 100644 --- a/src/compiler/bundle/ext-transforms-plugin.ts +++ b/src/compiler/bundle/ext-transforms-plugin.ts @@ -7,7 +7,12 @@ import { parseImportPath } from '../transformers/stencil-import-path'; import type { Plugin } from 'rollup'; import { runPluginTransformsEsmImports } from '../plugin/plugin'; -export const extTransformsPlugin = (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, bundleOpts: BundleOptions): Plugin => { +export const extTransformsPlugin = ( + config: d.Config, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, + bundleOpts: BundleOptions, +): Plugin => { return { name: 'extTransformsPlugin', @@ -67,6 +72,7 @@ export const extTransformsPlugin = (config: d.Config, compilerCtx: d.CompilerCtx // Track dependencies for (const dep of pluginTransforms.dependencies) { this.addWatchFile(dep); + compilerCtx.addWatchFile(dep); } buildCtx.diagnostics.push(...pluginTransforms.diagnostics); diff --git a/src/compiler/output-targets/output-service-workers.ts b/src/compiler/output-targets/output-service-workers.ts index 4cf7bea1856..3fb27eaa896 100644 --- a/src/compiler/output-targets/output-service-workers.ts +++ b/src/compiler/output-targets/output-service-workers.ts @@ -3,23 +3,24 @@ import { generateServiceWorker } from '../service-worker/generate-sw'; import { isOutputTargetWww } from './output-utils'; export const outputServiceWorkers = async (config: d.Config, buildCtx: d.BuildCtx) => { - const wwwServiceOutputs = config.outputTargets.filter(isOutputTargetWww).filter(o => typeof o.indexHtml === 'string' && !!o.serviceWorker); + const wwwServiceOutputs = config.outputTargets + .filter(isOutputTargetWww) + .filter(o => typeof o.indexHtml === 'string' && !!o.serviceWorker); - if (wwwServiceOutputs.length === 0) { - return; - } - - if (config.sys.lazyRequire == null) { + if (wwwServiceOutputs.length === 0 || config.sys.lazyRequire == null) { return; } // let's make sure they have what we need from workbox installed - await config.sys.lazyRequire.ensure(config.logger, config.rootDir, [WORKBOX_BUILD_MODULE_ID]); + const diagnostics = await config.sys.lazyRequire.ensure(config.rootDir, ['workbox-build']); + if (diagnostics.length > 0) { + buildCtx.diagnostics.push(...diagnostics); + } else { + // we've ensure workbox is installed, so let's require it now + const workbox: d.Workbox = config.sys.lazyRequire.require(config.rootDir, 'workbox-build'); - // we've ensure workbox is installed, so let's require it now - const workbox: d.Workbox = config.sys.lazyRequire.require(WORKBOX_BUILD_MODULE_ID); - - await Promise.all(wwwServiceOutputs.map(outputTarget => generateServiceWorker(config, buildCtx, workbox, outputTarget))); + await Promise.all( + wwwServiceOutputs.map(outputTarget => generateServiceWorker(config, buildCtx, workbox, outputTarget)), + ); + } }; - -const WORKBOX_BUILD_MODULE_ID = 'workbox-build'; diff --git a/src/compiler/style/global-styles.ts b/src/compiler/style/global-styles.ts index 45d1e3f4e69..b592b844ef3 100644 --- a/src/compiler/style/global-styles.ts +++ b/src/compiler/style/global-styles.ts @@ -30,12 +30,22 @@ const buildGlobalStyles = async (config: d.Config, compilerCtx: d.CompilerCtx, b try { globalStylePath = normalizePath(globalStylePath); + compilerCtx.addWatchFile(globalStylePath); const transformResults = await runPluginTransforms(config, compilerCtx, buildCtx, globalStylePath); if (transformResults) { - const optimizedCss = await optimizeCss(config, compilerCtx, buildCtx.diagnostics, transformResults.code, globalStylePath); + const optimizedCss = await optimizeCss( + config, + compilerCtx, + buildCtx.diagnostics, + transformResults.code, + globalStylePath, + ); compilerCtx.cachedGlobalStyle = optimizedCss; + if (Array.isArray(transformResults.dependencies)) { + transformResults.dependencies.forEach(dep => compilerCtx.addWatchFile(dep)); + } return optimizedCss; } } catch (e) { @@ -65,7 +75,14 @@ const canSkipGlobalStyles = async (config: d.Config, compilerCtx: d.CompilerCtx, return false; } - const hasChangedImports = await hasChangedImportFile(config, compilerCtx, buildCtx, config.globalStyle, compilerCtx.cachedGlobalStyle, []); + const hasChangedImports = await hasChangedImportFile( + config, + compilerCtx, + buildCtx, + config.globalStyle, + compilerCtx.cachedGlobalStyle, + [], + ); if (hasChangedImports) { return false; } @@ -73,7 +90,14 @@ const canSkipGlobalStyles = async (config: d.Config, compilerCtx: d.CompilerCtx, return true; }; -const hasChangedImportFile = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, filePath: string, content: string, noLoop: string[]): Promise => { +const hasChangedImportFile = async ( + config: d.Config, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, + filePath: string, + content: string, + noLoop: string[], +): Promise => { if (noLoop.includes(filePath)) { return false; } @@ -82,7 +106,14 @@ const hasChangedImportFile = async (config: d.Config, compilerCtx: d.CompilerCtx return hasChangedImportContent(config, compilerCtx, buildCtx, filePath, content, noLoop); }; -const hasChangedImportContent = async (config: d.Config, compilerCtx: d.CompilerCtx, buildCtx: d.BuildCtx, filePath: string, content: string, checkedFiles: string[]) => { +const hasChangedImportContent = async ( + config: d.Config, + compilerCtx: d.CompilerCtx, + buildCtx: d.BuildCtx, + filePath: string, + content: string, + checkedFiles: string[], +) => { const cssImports = await getCssImports(config, compilerCtx, buildCtx, filePath, content); if (cssImports.length === 0) { // don't bother diff --git a/src/declarations/stencil-private.ts b/src/declarations/stencil-private.ts index 18a2608c648..7368911aeae 100644 --- a/src/declarations/stencil-private.ts +++ b/src/declarations/stencil-private.ts @@ -12,7 +12,6 @@ import type { DevServerConfig, DevServerEditor, Diagnostic, - FsWatcher, FsWriteOptions, Logger, LoggerTimeSpan, @@ -615,6 +614,8 @@ export interface CompilerCtx { activeFilesAdded: string[]; activeFilesDeleted: string[]; activeFilesUpdated: string[]; + addWatchDir: (path: string, recursive: boolean) => void; + addWatchFile: (path: string) => void; cache: Cache; cachedStyleMeta: Map; cachedGlobalStyle: string; @@ -622,21 +623,16 @@ export interface CompilerCtx { compilerOptions: any; events: BuildEvents; fs: InMemoryFileSystem; - fsWatcher: FsWatcher; hasSuccessfulBuild: boolean; isActivelyBuilding: boolean; - lastComponentStyleInput: Map; lastBuildResults: CompilerBuildResults; - lastBuildStyles: Map; moduleMap: ModuleMap; nodeMap: NodeMap; resolvedCollections: Set; rollupCacheHydrate: any; rollupCacheLazy: any; rollupCacheNative: any; - rootTsFiles: string[]; styleModeNames: Set; - tsService: TsService; changedModules: Set; changedFiles: Set; worker?: CompilerWorkerContext; @@ -648,14 +644,6 @@ export interface CompilerCtx { export type NodeMap = WeakMap; -export type TsService = ( - compilerCtx: CompilerCtx, - buildCtx: BuildCtx, - tsFilePaths: string[], - checkCacheKey: boolean, - useFsCache: boolean, -) => Promise; - /** Must be serializable to JSON!! */ export interface ComponentCompilerFeatures { hasAttribute: boolean; @@ -1991,9 +1979,6 @@ export interface PackageJsonData { 'devDependencies'?: { [moduleId: string]: string; }; - 'lazyDependencies'?: { - [moduleId: string]: string; - }; 'repository'?: { type?: string; url?: string; diff --git a/src/declarations/stencil-public-compiler.ts b/src/declarations/stencil-public-compiler.ts index 8e1e4811611..6fd0b7504af 100644 --- a/src/declarations/stencil-public-compiler.ts +++ b/src/declarations/stencil-public-compiler.ts @@ -2094,15 +2094,9 @@ export interface OptimizeJsOutput { } export interface LazyRequire { - ensure(logger: Logger, fromDir: string, moduleIds: string[]): Promise; - require(moduleId: string): any; - getModulePath(moduleId: string): string; -} - -export interface FsWatcher { - addFile(path: string): Promise; - addDirectory(path: string): Promise; - close(): void; + ensure(fromDir: string, moduleIds: string[]): Promise; + require(fromDir: string, moduleId: string): any; + getModulePath(fromDir: string, moduleId: string): string; } export interface FsWatcherItem { diff --git a/src/sys/node/node-lazy-require.ts b/src/sys/node/node-lazy-require.ts index e983ecf10b5..94908d5fff4 100644 --- a/src/sys/node/node-lazy-require.ts +++ b/src/sys/node/node-lazy-require.ts @@ -1,177 +1,55 @@ import type * as d from '../../declarations'; -import path from 'path'; +import { buildError } from '@utils'; import { NodeResolveModule } from './node-resolve-module'; -import { readFile } from './node-fs-promisify'; -import { SpawnOptions, spawn } from 'child_process'; import semiver from 'semiver'; +import fs from 'graceful-fs'; +import path from 'path'; export class NodeLazyRequire implements d.LazyRequire { - private moduleData = new Map(); - - constructor(private nodeResolveModule: NodeResolveModule, private lazyDependencies: { [dep: string]: [string, string] }) {} - - async ensure(logger: d.Logger, fromDir: string, ensureModuleIds: string[]) { - const depsToInstall: DepToInstall[] = []; - let isUpdate = false; - - const promises = ensureModuleIds.map(async ensureModuleId => { - const existingModuleData = this.moduleData.get(ensureModuleId); - if (existingModuleData && existingModuleData.fromDir && existingModuleData.modulePath) { - return; + private ensured = new Set(); + + constructor( + private nodeResolveModule: NodeResolveModule, + private lazyDependencies: { [dep: string]: [string, string] }, + ) {} + + async ensure(fromDir: string, ensureModuleIds: string[]) { + const diagnostics: d.Diagnostic[] = []; + const missingDeps: string[] = []; + + ensureModuleIds.forEach(ensureModuleId => { + if (!this.ensured.has(ensureModuleId)) { + const [minVersion, recommendedVersion] = this.lazyDependencies[ensureModuleId]; + try { + const pkgJsonPath = this.nodeResolveModule.resolveModule(fromDir, ensureModuleId); + + const installedPkgJson: d.PackageJsonData = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8')); + + if (semiver(installedPkgJson.version, minVersion) >= 0) { + this.ensured.add(ensureModuleId); + return; + } + } catch (e) {} + missingDeps.push(`${ensureModuleId}@${recommendedVersion}`); } - - const [minVersion, maxVersion] = this.lazyDependencies[ensureModuleId]; - - try { - const resolvedPkgJsonPath = this.nodeResolveModule.resolveModule(fromDir, ensureModuleId); - - const installedPkgJson = await readPackageJson(resolvedPkgJsonPath); - - isUpdate = true; - - if (semiver(installedPkgJson.version, minVersion) >= 0) { - this.moduleData.set(ensureModuleId, { - fromDir: fromDir, - modulePath: path.dirname(resolvedPkgJsonPath), - }); - return; - } - } catch (e) {} - - depsToInstall.push({ - moduleId: ensureModuleId, - requiredVersionRange: maxVersion, - }); }); - await Promise.all(promises); - - if (depsToInstall.length === 0) { - return Promise.resolve(); + if (missingDeps.length > 0) { + const err = buildError(diagnostics); + err.header = `Please install missing dev dependencies with either npm or yarn.`; + err.messageText = `npm install --save-dev ${missingDeps.join(' ')}`; } - const msg = `Please wait while required dependencies are ${isUpdate ? `updated` : `installed`}. This may take a few moments and will only be required for the initial run.`; - - logger.info(logger.magenta(msg)); - - const moduleIds = depsToInstall.map(dep => dep.moduleId); - const timeSpan = logger.createTimeSpan(`installing dependenc${moduleIds.length > 1 ? 'ies' : 'y'}: ${moduleIds.join(', ')}`); - - try { - const installModules = depsToInstall.map(dep => { - let moduleId = dep.moduleId; - if (dep.requiredVersionRange) { - moduleId += '@' + dep.requiredVersionRange; - } - return moduleId; - }); - - await npmInstall(logger, fromDir, installModules); - - depsToInstall.forEach(installedDep => { - this.moduleData.set(installedDep.moduleId, { - fromDir: fromDir, - modulePath: null, - }); - }); - - timeSpan.finish(`installing dependencies finished`); - } catch (e) { - logger.error(`lazy require failed: ${e}`); - } + return diagnostics; } - require(moduleId: string) { - const moduleData = this.moduleData.get(moduleId); - - if (!moduleData) { - throw new Error(`lazy required module has not been ensured: ${moduleId}`); - } - - if (!moduleData.modulePath) { - const modulePkgJsonPath = this.nodeResolveModule.resolveModule(moduleData.fromDir, moduleId); - moduleData.modulePath = path.dirname(modulePkgJsonPath); - this.moduleData.set(moduleId, moduleData); - } - - return require(moduleData.modulePath); + require(fromDir: string, moduleId: string) { + const modulePath = this.getModulePath(fromDir, moduleId); + return require(modulePath); } - getModulePath(moduleId: string) { - const moduleData = this.moduleData.get(moduleId); - - if (!moduleData) { - throw new Error(`lazy required module has not been ensured: ${moduleId}`); - } - - if (!moduleData.modulePath) { - const modulePkgJsonPath = this.nodeResolveModule.resolveModule(moduleData.fromDir, moduleId); - moduleData.modulePath = path.dirname(modulePkgJsonPath); - this.moduleData.set(moduleId, moduleData); - } - - return moduleData.modulePath; + getModulePath(fromDir: string, moduleId: string) { + const modulePath = this.nodeResolveModule.resolveModule(fromDir, moduleId); + return path.dirname(modulePath); } } - -function npmInstall(logger: d.Logger, fromDir: string, moduleIds: string[]) { - return new Promise((resolve, reject) => { - const cmd = 'npm'; - - const args = ['install', ...moduleIds, '--no-audit', '--save-exact', '--save-dev']; - - const opts: SpawnOptions = { - shell: true, - cwd: fromDir, - env: Object.assign({}, process.env), - }; - opts.env.NODE_ENV = 'development'; - - if (logger.getLevel() === 'debug') { - args.push('--verbose'); - } - - logger.debug(`${cmd} ${args.join(' ')}`); - logger.debug(`${cmd}, cwd: ${fromDir}`); - - const childProcess = spawn(cmd, args, opts); - - let output = ''; - - if (childProcess.stdout) { - childProcess.stdout.setEncoding('utf8'); - childProcess.stdout.on('data', data => { - output += data; - }); - } - - if (childProcess.stderr) { - childProcess.stderr.setEncoding('utf8'); - childProcess.stderr.on('data', data => { - output += data; - }); - } - - childProcess.once('exit', exitCode => { - if (logger.getLevel() === 'debug') { - logger.debug(`${cmd}, exit ${exitCode}`); - } - - if (exitCode === 0) { - resolve(); - } else { - reject(`failed to install: ${moduleIds.join(', ')}${output ? ', ' + output : ''}`); - } - }); - }); -} - -async function readPackageJson(pkgJsonPath: string) { - const data = await readFile(pkgJsonPath, 'utf8'); - return JSON.parse(data) as d.PackageJsonData; -} - -interface DepToInstall { - moduleId: string; - requiredVersionRange: string; -} diff --git a/src/sys/node/node-sys.ts b/src/sys/node/node-sys.ts index 9d17d93c439..20061610658 100644 --- a/src/sys/node/node-sys.ts +++ b/src/sys/node/node-sys.ts @@ -400,8 +400,7 @@ export function createNodeSys(c: { process?: any } = {}) { const tsFileWatcher = tsSysWatchDirectory( p, fileName => { - fileName = normalizePath(fileName); - callback(fileName, null); + callback(normalizePath(fileName), null); }, recursive, ); @@ -541,6 +540,7 @@ export function createNodeSys(c: { process?: any } = {}) { const nodeResolve = new NodeResolveModule(); sys.lazyRequire = new NodeLazyRequire(nodeResolve, { + // [minimumVersion, recommendedVersion] '@types/jest': ['24.9.1', '26.0.10'], '@types/puppeteer': ['1.19.0', '3.0.1'], 'jest': ['24.9.0', '26.4.1'], diff --git a/src/testing/mocks.ts b/src/testing/mocks.ts index 5ce85d3830e..8c8bb5aff3e 100644 --- a/src/testing/mocks.ts +++ b/src/testing/mocks.ts @@ -7,6 +7,7 @@ import { createWorkerContext } from '@stencil/core/compiler'; import { MockWindow } from '@stencil/core/mock-doc'; import { TestingLogger } from './testing-logger'; import path from 'path'; +import { noop } from '@utils'; export function mockConfig(sys?: CompilerSystem) { const rootDir = path.resolve('/'); @@ -59,34 +60,29 @@ export function mockCompilerCtx(config?: Config) { activeFilesAdded: [], activeFilesDeleted: [], activeFilesUpdated: [], - fs: null, + addWatchDir: noop, + addWatchFile: noop, cachedGlobalStyle: null, + changedFiles: new Set(), + changedModules: new Set(), collections: [], compilerOptions: null, cache: null, cachedStyleMeta: new Map(), events: null, - fsWatcher: null, + fs: null, hasSuccessfulBuild: false, isActivelyBuilding: false, - lastComponentStyleInput: new Map(), lastBuildResults: null, - lastBuildStyles: null, moduleMap: new Map(), nodeMap: new WeakMap(), + reset: noop, resolvedCollections: new Set(), + rollupCache: new Map(), rollupCacheHydrate: null, rollupCacheLazy: null, rollupCacheNative: null, - rollupCache: new Map(), - rootTsFiles: [], styleModeNames: new Set(), - tsService: null, - changedModules: new Set(), - changedFiles: new Set(), - reset: () => { - /**/ - }, worker: createWorkerContext(config.sys), }; diff --git a/src/testing/puppeteer/puppeteer-browser.ts b/src/testing/puppeteer/puppeteer-browser.ts index 40238e44a2a..b362ec2e510 100644 --- a/src/testing/puppeteer/puppeteer-browser.ts +++ b/src/testing/puppeteer/puppeteer-browser.ts @@ -9,8 +9,8 @@ export async function startPuppeteerBrowser(config: Config) { const env: E2EProcessEnv = process.env; const puppeteerDep = config.testing.browserExecutablePath ? 'puppeteer-core' : 'puppeteer'; - const puppeteerModulePath = config.sys.lazyRequire.getModulePath(puppeteerDep); - const puppeteer = require(puppeteerModulePath); + const puppeteerModulePath = config.sys.lazyRequire.getModulePath(config.rootDir, puppeteerDep); + const puppeteer = config.sys.lazyRequire.require(config.rootDir, puppeteerModulePath); env.__STENCIL_PUPPETEER_MODULE__ = puppeteerModulePath; env.__STENCIL_BROWSER_WAIT_UNTIL = config.testing.browserWaitUntil;