/** * 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 { promisify } from 'util'; import * as path from 'path'; import { promises as fsp } from 'fs'; import glob from 'glob'; const globP = promisify(glob); const autoGenComment = '// This file is autogenerated by lib/feature-plugin.js\n'; export default function () { let previousWorkerImports; let previousJoinedMetas; /** * Generates the worker file & tsconfig for all features * * @param {string[]} workerImports */ async function generateWorkerFile(workerImports) { const workerBasePath = path.join(process.cwd(), 'src', 'features-worker'); const featuresWorkerTsNames = workerImports.map((tsImport) => [ path.relative(workerBasePath, tsImport), path.basename(tsImport), ]); const workerFile = [ autoGenComment, `import { expose } from 'comlink';`, `import { timed } from './util';`, featuresWorkerTsNames.map( ([path, name]) => `import ${name} from './${path}';`, ), `const exports = {`, featuresWorkerTsNames.map(([_, name]) => [ ` ${name}(`, ` ...args: Parameters`, ` ): ReturnType {`, ` return timed('${name}', () => ${name}(...args));`, ` },`, ]), `};`, `export type ProcessorWorkerApi = typeof exports;`, `expose(exports, self);`, ] .flat(Infinity) .join('\n'); const workerTsConfig = { extends: '../../generic-tsconfig.json', compilerOptions: { lib: ['webworker', 'esnext'], }, references: featuresWorkerTsNames.map(([tsImport]) => ({ path: path.dirname(tsImport), })), }; await Promise.all([ fsp.writeFile( path.join(workerBasePath, 'tsconfig.json'), JSON.stringify(workerTsConfig, null, ' '), ), fsp.writeFile(path.join(workerBasePath, 'index.ts'), workerFile), ]); } /** * Generates the client JS to call worker methods. * * @param {string[]} workerImports */ async function generateWorkerBridge(workerImports) { const workerBridgeBasePath = path.join( process.cwd(), 'src', 'client', 'lazy-app', 'worker-bridge', ); const featuresWorkerBridgeTsNames = workerImports.map((tsImport) => [ path.relative(workerBridgeBasePath, tsImport), path.basename(tsImport), ]); const bridgeMeta = [ autoGenComment, featuresWorkerBridgeTsNames.map( ([path, name]) => `import type ${name} from '${path}';`, ), `export const methodNames = ${JSON.stringify( featuresWorkerBridgeTsNames.map(([_, name]) => name), null, ' ', )} as const;`, `export interface BridgeMethods {`, featuresWorkerBridgeTsNames.map(([_, name]) => [ ` ${name}(`, ` signal: AbortSignal,`, ` ...args: Parameters`, ` ): Promise>;`, ]), `}`, ] .flat(Infinity) .join('\n'); await fsp.writeFile(path.join(workerBridgeBasePath, 'meta.ts'), bridgeMeta); } async function generateWorkerFiles() { const workerImports = ( await globP('src/features/*/**/worker/*.ts', { absolute: true, }) ) .filter((tsFile) => !tsFile.endsWith('.d.ts')) .map((tsFile) => tsFile.slice(0, -'.ts'.length)); const joinedWorkerImports = workerImports.join(); // Avoid regenerating if nothing's changed. // This also prevents an infinite loop in the watcher. if (joinedWorkerImports === previousWorkerImports) return; previousWorkerImports = joinedWorkerImports; await Promise.all([ generateWorkerFile(workerImports), generateWorkerBridge(workerImports), ]); } async function generateFeatureMeta() { const encoderMetas = ( await globP('src/features/encoders/*/shared/meta.ts', { absolute: true, }) ) .filter((tsFile) => !tsFile.endsWith('.d.ts')) .map((tsFile) => tsFile.slice(0, -'.ts'.length)); const processorMetas = ( await globP('src/features/processors/*/shared/meta.ts', { absolute: true, }) ) .filter((tsFile) => !tsFile.endsWith('.d.ts')) .map((tsFile) => tsFile.slice(0, -'.ts'.length)); const featureMetaBasePath = path.join( process.cwd(), 'src', 'client', 'lazy-app', 'feature-meta', ); const joinedMetas = [...encoderMetas, ...processorMetas].join(); // Avoid regenerating if nothing's changed. // This also prevents an infinite loop in the watcher. if (joinedMetas === previousJoinedMetas) return; previousJoinedMetas = joinedMetas; const getTsName = (tsImport) => [ path.relative(featureMetaBasePath, tsImport), path.basename(tsImport.slice(0, -'/shared/meta'.length)), ]; const encoderMetaTsNames = encoderMetas.map(getTsName); const processorMetaTsNames = processorMetas.map(getTsName); const featureMeta = [ autoGenComment, // Encoder stuff encoderMetaTsNames.map( ([path, name]) => `import * as ${name}EncoderMeta from '${path}';`, ), `export type EncoderState =`, encoderMetaTsNames.map( ([_, name]) => ` | { type: "${name}", options: ${name}EncoderMeta.EncodeOptions }`, ), `;`, `export type EncoderOptions =`, encoderMetaTsNames.map( ([_, name]) => ` | ${name}EncoderMeta.EncodeOptions`, ), `;`, `export const encoderMap = {`, encoderMetaTsNames.map(([_, name]) => ` ${name}: ${name}EncoderMeta,`), `};`, `export type EncoderType = keyof typeof encoderMap`, `export const encoders = [...Object.values(encoderMap)];`, // Processor stuff processorMetaTsNames.map( ([path, name]) => `import * as ${name}ProcessorMeta from '${path}';`, ), `interface Enableable { enabled: boolean; }`, `export interface ProcessorState {`, processorMetaTsNames.map( ([_, name]) => ` ${name}: Enableable & ${name}ProcessorMeta.Options;`, ), `}`, `export const defaultProcessorState: ProcessorState = {`, processorMetaTsNames.map( ([_, name]) => ` ${name}: { enabled: false, ...${name}ProcessorMeta.defaultOptions },`, ), `}`, ] .flat(Infinity) .join('\n'); await fsp.writeFile( path.join(featureMetaBasePath, 'index.ts'), featureMeta, ); } return { name: 'feature-plugin', async buildStart() { await Promise.all([generateWorkerFiles(), generateFeatureMeta()]); }, }; }