Skip to content

Commit 2f7bfae

Browse files
authored
fix: improve sass compile speed by preloading files (#6881)
1 parent 0752669 commit 2f7bfae

File tree

6 files changed

+188
-69
lines changed

6 files changed

+188
-69
lines changed

packages/app/src/sandbox/eval/transpilers/sass/worker/resolver.ts

Lines changed: 49 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,10 @@ export function getPossibleSassPaths(
4545
return directories.map(directory => pathUtils.join(directory, filepath));
4646
}
4747

48-
async function resolveAsyncModule(opts: {
48+
export async function resolveAsyncModule(opts: {
4949
path: string;
5050
options: {
51+
isAbsolute: boolean;
5152
ignoredExtensions: typeof POTENTIAL_EXTENSIONS;
5253
};
5354
loaderContextId: number;
@@ -91,6 +92,7 @@ function existsPromise(opts: {
9192
const resolvedModule = await resolveAsyncModule({
9293
path: filepath,
9394
options: {
95+
isAbsolute: filepath.startsWith('/'),
9496
ignoredExtensions: POTENTIAL_EXTENSIONS,
9597
},
9698
loaderContextId,
@@ -103,6 +105,19 @@ function existsPromise(opts: {
103105
return;
104106
}
105107

108+
try {
109+
fs.mkdirSync(pathUtils.dirname(resolvedModule.path), {
110+
recursive: true,
111+
});
112+
} catch (e) {
113+
/* noop */
114+
}
115+
try {
116+
fs.writeFileSync(resolvedModule.path, resolvedModule.code);
117+
} catch (e) {
118+
/* noop */
119+
}
120+
106121
r(resolvedModule.path);
107122
} catch (e) {
108123
r(false);
@@ -114,43 +129,6 @@ function existsPromise(opts: {
114129
});
115130
}
116131

117-
/**
118-
* Return and stop as soon as one promise returns a truthy value
119-
*/
120-
function firstTrue<T>(promises: Array<Promise<T | false>>): Promise<T> {
121-
if (!promises.length) {
122-
return Promise.reject(new Error('No promises supplied'));
123-
}
124-
125-
return new Promise((resolvePromise, rejectPromise) => {
126-
let resolved: boolean = false;
127-
let firstError: any;
128-
129-
Promise.all(
130-
promises.map(promise =>
131-
promise.then(
132-
result => {
133-
if (result && !resolved) {
134-
resolved = true;
135-
resolvePromise(result);
136-
}
137-
},
138-
err => {
139-
firstError = firstError || err;
140-
}
141-
)
142-
)
143-
).then(() => {
144-
if (!resolved) {
145-
const err =
146-
firstError ||
147-
new Error('None of the promises returned a truthy value');
148-
rejectPromise(err);
149-
}
150-
});
151-
});
152-
}
153-
154132
async function resolvePotentialPath(opts: {
155133
fs: any;
156134
potentialPath: string;
@@ -163,17 +141,36 @@ async function resolvePotentialPath(opts: {
163141
const pathVariations = getPathVariations(
164142
pathUtils.basename(potentialPath)
165143
).map(variation => pathUtils.join(pathDirName, variation));
166-
const foundFilePath = await firstTrue<string>(
167-
pathVariations.map(path =>
168-
existsPromise({ fs, filepath: path, loaderContextId, childHandler })
169-
)
170-
);
171-
172-
if (!foundFilePath) {
173-
return null;
144+
145+
// Try the first one first, that's often the right one and this way we don't spam
146+
// the main thread.
147+
const firstPath = pathVariations.shift();
148+
const firstFoundFilePath = await existsPromise({
149+
fs,
150+
filepath: firstPath,
151+
loaderContextId,
152+
childHandler,
153+
});
154+
155+
if (firstFoundFilePath) {
156+
return firstPath;
157+
}
158+
159+
for (const path of pathVariations) {
160+
// eslint-disable-next-line no-await-in-loop
161+
const result = await existsPromise({
162+
fs,
163+
filepath: path,
164+
loaderContextId,
165+
childHandler,
166+
});
167+
168+
if (result) {
169+
return path;
170+
}
174171
}
175172

176-
return foundFilePath;
173+
return null;
177174
} catch (err) {
178175
return null;
179176
}
@@ -198,7 +195,7 @@ interface ISassResolverOptions {
198195
includePaths?: Array<string>;
199196
env?: any;
200197
fs: any;
201-
resolutionCache: { [k: string]: string };
198+
resolutionCache: { [k: string]: Promise<string> };
202199
loaderContextId: number;
203200
childHandler: ChildHandler;
204201
}
@@ -249,15 +246,16 @@ export async function resolveSassUrl(opts: ISassResolverOptions) {
249246
return resolutionCache[potentialPath];
250247
}
251248

252-
// eslint-disable-next-line no-await-in-loop
253-
const resolvedPath = await resolvePotentialPath({
249+
resolutionCache[potentialPath] = resolvePotentialPath({
254250
fs,
255251
potentialPath,
256252
loaderContextId,
257253
childHandler,
258254
});
255+
256+
// eslint-disable-next-line no-await-in-loop
257+
const resolvedPath = await resolutionCache[potentialPath];
259258
if (resolvedPath) {
260-
resolutionCache[potentialPath] = resolvedPath;
261259
return resolvedPath;
262260
}
263261
}

packages/app/src/sandbox/eval/transpilers/sass/worker/sass-worker.ts

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import delay from '@codesandbox/common/lib/utils/delay';
22

3-
import { resolveSassUrl } from './resolver';
3+
import { resolveAsyncModule, resolveSassUrl } from './resolver';
44
import { ChildHandler } from '../../worker-transpiler/child-handler';
5+
import { getModulesFromMainThread } from '../../utils/fs';
56

67
// @ts-ignore
78
self.window = self;
@@ -16,6 +17,7 @@ async function fetchSassLibrary() {
1617
}
1718

1819
let fsInitialized = false;
20+
let fsLoading = false;
1921
const childHandler = new ChildHandler('sass-worker');
2022

2123
interface ISassCompileOptions {
@@ -25,8 +27,10 @@ interface ISassCompileOptions {
2527
loaderContextId: number;
2628
}
2729

28-
async function initFS() {
29-
if (!fsInitialized) {
30+
async function initFS(loaderContextId: number) {
31+
if (!fsLoading && !fsInitialized) {
32+
await initializeBrowserFS(loaderContextId);
33+
} else if (!fsInitialized) {
3034
while (!fsInitialized) {
3135
await delay(50); // eslint-disable-line
3236
}
@@ -38,7 +42,7 @@ async function compileSass(opts: ISassCompileOptions) {
3842

3943
const Sass = await fetchSassLibrary();
4044

41-
await initFS();
45+
await initFS(loaderContextId);
4246

4347
// @ts-ignore
4448
// eslint-disable-next-line
@@ -54,8 +58,24 @@ async function compileSass(opts: ISassCompileOptions) {
5458
if (!foundFileCache[filepath]) {
5559
foundFileCache[filepath] = new Promise(
5660
(promiseResolve, promiseReject) => {
57-
fs.readFile(filepath, {}, (error, data) => {
61+
fs.readFile(filepath, {}, async (error, data) => {
5862
if (error) {
63+
// Try to download it
64+
const module = await resolveAsyncModule({
65+
path: filepath,
66+
loaderContextId: opts.loaderContextId,
67+
childHandler,
68+
options: {
69+
isAbsolute: false,
70+
ignoredExtensions: ['.sass', '.css', '.scss'],
71+
},
72+
});
73+
74+
if (module) {
75+
promiseResolve(module.code);
76+
return;
77+
}
78+
5979
promiseReject(error);
6080
return;
6181
}
@@ -146,24 +166,56 @@ async function compileSass(opts: ISassCompileOptions) {
146166
};
147167
}
148168

149-
async function initializeBrowserFS() {
150-
await new Promise(res => {
169+
async function initializeBrowserFS(loaderContextId: number) {
170+
fsLoading = true;
171+
172+
const modules = await getModulesFromMainThread({
173+
childHandler,
174+
loaderContextId,
175+
});
176+
177+
const tModules = {};
178+
modules.forEach(module => {
179+
tModules[module.path] = { module };
180+
});
181+
182+
const bfsWrapper = {
183+
getTranspiledModules: () => tModules,
184+
addModule: () => {},
185+
removeModule: () => {},
186+
moveModule: () => {},
187+
updateModule: () => {},
188+
};
189+
190+
return new Promise(resolvePromise => {
151191
// @ts-ignore
152-
// eslint-disable-next-line
153192
BrowserFS.configure(
154193
{
155-
fs: 'WorkerFS',
156-
options: { worker: self },
194+
fs: 'OverlayFS',
195+
options: {
196+
writable: { fs: 'InMemory' },
197+
readable: {
198+
fs: 'CodeSandboxFS',
199+
options: {
200+
manager: bfsWrapper,
201+
},
202+
},
203+
},
157204
},
158-
() => {
159-
res(null);
205+
err => {
206+
if (err) {
207+
console.error(err);
208+
return;
209+
}
210+
fsLoading = false;
211+
fsInitialized = true;
212+
resolvePromise(null);
213+
// BrowserFS is initialized and ready-to-use!
160214
}
161215
);
162216
});
163-
164-
fsInitialized = true;
165217
}
166218

167219
childHandler.registerFunction('compile', compileSass);
168-
childHandler.registerFSInitializer(initializeBrowserFS);
220+
childHandler.registerFSInitializer(() => {});
169221
childHandler.emitReady();

packages/sandpack-core/src/npm/dynamic/fetch-npm-module.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function prependRootPath(meta: Meta, rootPath: string): Meta {
4040
export interface FetchProtocol {
4141
file(name: string, version: string, path: string): Promise<string>;
4242
meta(name: string, version: string): Promise<Meta>;
43+
massFiles?(name: string, version: string): Promise<Array<Module>>;
4344
}
4445

4546
export function setCombinedMetas(givenCombinedMetas: Meta) {
@@ -94,6 +95,33 @@ function getMeta(
9495
}));
9596
}
9697

98+
const downloadedAllDependencies = new Set<string>();
99+
/**
100+
* If the protocol supports it, download all all files of the dependency
101+
* at once. It's an optimization.
102+
*/
103+
export async function downloadAllDependencyFiles(
104+
name: string,
105+
version: string
106+
): Promise<Module[] | null> {
107+
if (downloadedAllDependencies.has(`${name}@${version}`)) {
108+
return null;
109+
}
110+
111+
downloadedAllDependencies.add(`${name}@${version}`);
112+
113+
const [depName, depVersion] = resolveNPMAlias(name, version);
114+
const nameWithoutAlias = depName.replace(ALIAS_REGEX, '');
115+
const protocol = getFetchProtocol(depName, depVersion);
116+
117+
if (protocol.massFiles) {
118+
// If the protocol supports returning many files at once, we opt for that instead.
119+
return protocol.massFiles(nameWithoutAlias, depVersion);
120+
}
121+
122+
return null;
123+
}
124+
97125
export function downloadDependency(
98126
name: string,
99127
version: string,

packages/sandpack-core/src/npm/dynamic/fetch-protocols/npm-registry.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { satisfies, valid } from 'semver';
2+
import { Module } from '../../../types/module';
23
import { FetchProtocol, Meta } from '../fetch-npm-module';
34
import { fetchWithRetries } from './utils';
45
import { TarStore } from './utils/tar-store';
@@ -235,6 +236,17 @@ export class NpmRegistryFetcher implements FetchProtocol {
235236
return this.tarStore.meta(name, tarball, this.getRequestInit());
236237
}
237238

239+
public async massFiles(name: string, version: string): Promise<Module[]> {
240+
const versionInfo = await this.getVersionInfo(name, version);
241+
242+
const tarball = this.getTarballUrl(
243+
name,
244+
versionInfo.version,
245+
versionInfo.dist.tarball
246+
);
247+
return this.tarStore.massFiles(name, tarball, this.getRequestInit());
248+
}
249+
238250
public condition = (name: string, version: string): boolean => {
239251
if (this.scopeWhitelist) {
240252
return this.scopeWhitelist.some(scope => {

packages/sandpack-core/src/npm/dynamic/fetch-protocols/utils/tar-store.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import untar, { UntarredFiles } from 'isomorphic-untar-gzip';
2+
import { Module } from '../../../../types/module';
23

3-
import { Meta } from '../../fetch-npm-module';
4+
import { FetchProtocol, Meta } from '../../fetch-npm-module';
45
import { fetchWithRetries } from '../utils';
56

67
type DeserializedFetchedTar = {
@@ -11,7 +12,7 @@ type DeserializedFetchedTar = {
1112
* Responsible for fetching, caching and converting tars to a structure that sandpack
1213
* understands and can use
1314
*/
14-
export class TarStore {
15+
export class TarStore implements FetchProtocol {
1516
private fetchedTars: {
1617
[key: string]: Promise<{ [path: string]: DeserializedFetchedTar }>;
1718
} = {};
@@ -77,4 +78,20 @@ export class TarStore {
7778

7879
return meta;
7980
}
81+
82+
async massFiles(
83+
name: string,
84+
url: string,
85+
requestInit?: RequestInit
86+
): Promise<Module[]> {
87+
const tarKey = this.generateKey(name, url);
88+
const tar = await (this.fetchedTars[tarKey] ||
89+
this.fetchTar(name, url, requestInit));
90+
91+
return Object.keys(tar).map(path => ({
92+
path,
93+
code: tar[path].content,
94+
downloaded: true,
95+
}));
96+
}
8097
}

0 commit comments

Comments
 (0)