/** * Copyright 2020 Google Inc. All Rights Reserved. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { spawn } from 'child_process'; import { relative, join } from 'path'; import { promises as fsp } from 'fs'; import { promisify } from 'util'; import * as ts from 'typescript'; import glob from 'glob'; import { sync as whichSync } from 'which'; const globP = promisify(glob); const tscPath = whichSync('tsc'); const extRe = /\.tsx?$/; function loadConfig(mainPath) { const fileName = ts.findConfigFile(mainPath, ts.sys.fileExists); if (!fileName) throw Error('tsconfig not found'); const text = ts.sys.readFile(fileName); const loadedConfig = ts.parseConfigFileTextToJson(fileName, text).config; const parsedTsConfig = ts.parseJsonConfigFileContent( loadedConfig, ts.sys, process.cwd(), undefined, fileName, ); return parsedTsConfig; } export default function simpleTS(mainPath, { noBuild, watch } = {}) { const config = loadConfig(mainPath); const args = ['-b', mainPath]; let tsBuildDone; async function watchBuiltFiles(rollupContext) { const matches = await globP(config.options.outDir + '/**/*.js'); for (const match of matches) rollupContext.addWatchFile(match); } async function tsBuild(rollupContext) { if (tsBuildDone) { // Watch lists are cleared on each build, so we need to rewatch all the JS files. await watchBuiltFiles(rollupContext); return tsBuildDone; } if (noBuild) { return (tsBuildDone = Promise.resolve()); } tsBuildDone = Promise.resolve().then(async () => { await new Promise((resolve) => { const proc = spawn(tscPath, args, { stdio: 'inherit', }); proc.on('exit', (code) => { if (code !== 0) { throw Error('TypeScript build failed'); } resolve(); }); }); await watchBuiltFiles(rollupContext); if (watch) { tsBuildDone.then(() => { spawn(tscPath, [...args, '--watch', '--preserveWatchOutput'], { stdio: 'inherit', }); }); } }); return tsBuildDone; } return { name: 'simple-ts', resolveId(id, importer) { // If there isn't an importer, it's an entry point, so we don't need to resolve it relative // to something. if (!importer) return null; const tsResolve = ts.resolveModuleName( id, importer, config.options, ts.sys, ); if ( // It didn't find anything !tsResolve.resolvedModule || // Or if it's linking to a definition file, it's something in node_modules, // or something local like css.d.ts tsResolve.resolvedModule.extension === '.d.ts' ) { return null; } return tsResolve.resolvedModule.resolvedFileName; }, async load(id) { if (!extRe.test(id)) return null; // TypeScript building is deferred until the first TS file load. // This allows prerequisites to happen first, // such as css.d.ts generation in css-plugin. await tsBuild(this); // Look for the JS equivalent in the tmp folder const newId = join( config.options.outDir, relative(config.options.rootDir, id), ).replace(extRe, '.js'); return fsp.readFile(newId, { encoding: 'utf8' }); }, }; }