/** * 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 { posix } from 'path'; import glob from 'glob'; import { promises as fsp } from 'fs'; 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).split(path.sep).join(posix.sep), 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;`, `// 'as any' to work around the way our client code has insight into worker code`, `expose(exports, self as any);`, ] .flat(Infinity) .join('\n'); await 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) .split(path.sep) .join(posix.sep), 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 getTsFiles = (glob) => globP(glob, { absolute: true, }).then((paths) => paths .filter((tsFile) => !tsFile.endsWith('.d.ts')) .map((tsFile) => tsFile.slice(0, -'.ts'.length)), ); const metas = await Promise.all( [ 'src/features/encoders/*/shared/meta.ts', 'src/features/processors/*/shared/meta.ts', 'src/features/preprocessors/*/shared/meta.ts', ].map((glob) => getTsFiles(glob)), ); const [encoderMetas, processorMetas, preprocessorMetas] = metas; const featureMetaBasePath = path.join( process.cwd(), 'src', 'client', 'lazy-app', 'feature-meta', ); const joinedMetas = metas.flat().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) .split(path.sep) .join(posix.sep), path.basename(tsImport.slice(0, -'/shared/meta'.length)), ]; const encoderMetaTsNames = encoderMetas.map((tsImport) => getTsName(tsImport), ); const processorMetaTsNames = processorMetas.map((tsImport) => getTsName(tsImport), ); const preprocessorMetaTsNames = preprocessorMetas.map((tsImport) => getTsName(tsImport), ); const featureMeta = [ autoGenComment, // Encoder stuff encoderMetaTsNames.map( ([path, name]) => `import * as ${name}EncoderMeta from '${path}';`, ), encoderMetaTsNames.map( ([path, name]) => `import * as ${name}EncoderEntry from '${path.replace( /shared\/meta$/, 'client', )}';`, ), `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}: { meta: ${name}EncoderMeta, ...${name}EncoderEntry },`, ), `};`, `export type EncoderType = keyof typeof encoderMap`, // Processor stuff processorMetaTsNames.map( ([path, name]) => `import * as ${name}ProcessorMeta from '${path}';`, ), `interface Enableable { enabled: boolean; }`, `export interface ProcessorOptions {`, processorMetaTsNames.map( ([_, name]) => ` ${name}: ${name}ProcessorMeta.Options;`, ), `}`, `export interface ProcessorState {`, processorMetaTsNames.map( ([_, name]) => ` ${name}: Enableable & ${name}ProcessorMeta.Options;`, ), `}`, `export const defaultProcessorState: ProcessorState = {`, processorMetaTsNames.map( ([_, name]) => ` ${name}: { enabled: false, ...${name}ProcessorMeta.defaultOptions },`, ), `}`, // Preprocessor stuff preprocessorMetaTsNames.map( ([path, name]) => `import * as ${name}PreprocessorMeta from '${path}';`, ), `export interface PreprocessorState {`, preprocessorMetaTsNames.map( ([_, name]) => ` ${name}: ${name}PreprocessorMeta.Options,`, ), `}`, `export const defaultPreprocessorState: PreprocessorState = {`, preprocessorMetaTsNames.map( ([_, name]) => ` ${name}: ${name}PreprocessorMeta.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()]); }, }; }