Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add optional outListingFile config property that copies model listing JSON file as post-generate step #493

Merged
merged 3 commits into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 4 additions & 12 deletions examples/house-game/sde.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,11 @@ export async function config() {
}
},

plugins: [
// Copy the generated model listing to the app so that it can be loaded
// at runtime
{
postGenerate: async context => {
const srcPath = joinPath(context.config.prepDir, 'build', 'processed.json')
const dstName = 'listing.json'
const stagedFilePath = context.prepareStagedFile('model', dstName, generatedFilePath(), dstName)
await copyFile(srcPath, stagedFilePath)
return true
}
},
// Copy the generated model listing to the app so that it can be loaded
// at runtime
outListingFile: generatedFilePath('listing.json'),

plugins: [
// Generate a `worker.js` file that runs the generated model in a worker
workerPlugin({
outputPaths: [generatedFilePath('worker.js')]
Expand Down
1 change: 1 addition & 0 deletions packages/build/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
dist
docs/entry.md
sde-prep
tests/build-prod/outputs
9 changes: 9 additions & 0 deletions packages/build/docs/interfaces/ResolvedConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,12 @@ ___
The code format to generate. If 'js', the model will be compiled to a JavaScript
file. If 'c', the model will be compiled to a C file (in which case an additional
plugin will be needed to convert the C code to a WebAssembly module).

___

### outListingFile

`Optional` **outListingFile**: `string`

The absolute path to the JSON file that will be written by the build process that
lists all dimensions and variables in the model.
10 changes: 10 additions & 0 deletions packages/build/docs/interfaces/UserConfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ defaults to 'js'.

___

### outListingFile

`Optional` **outListingFile**: `string`

If defined, the build process will write a JSON file to the provided path that lists
all dimensions and variables in the model. This can be an absolute path, or if it
is a relative path it will be resolved relative to the `rootDir` for the project.

___

### plugins

`Optional` **plugins**: [`Plugin`](Plugin.md)[]
Expand Down
6 changes: 6 additions & 0 deletions packages/build/src/_shared/resolved-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export interface ResolvedConfig {
*/
genFormat: 'js' | 'c'

/**
* The absolute path to the JSON file that will be written by the build process that
* lists all dimensions and variables in the model.
*/
outListingFile?: string

/**
* The path to the `@sdeverywhere/cli` package. This is currently only used to get
* access to the files in the `src/c` directory.
Expand Down
13 changes: 12 additions & 1 deletion packages/build/src/build/impl/gen-model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2022 Climate Interactive / New Venture Fund

import { copyFile, readdir, readFile, writeFile } from 'fs/promises'
import { join as joinPath } from 'path'
import { basename, dirname, join as joinPath } from 'path'

import { log } from '../../_shared/log'

Expand Down Expand Up @@ -87,6 +87,17 @@ export async function generateModel(context: BuildContext, plugins: Plugin[]): P
await copyFile(generatedCodePath, stagedOutputJsPath)
}

if (config.outListingFile) {
// Copy the model listing file
const srcListingJsonPath = joinPath(prepDir, 'build', 'processed.json')
const stagedDir = 'model'
const stagedFile = 'listing.json'
const dstDir = dirname(config.outListingFile)
const dstFile = basename(config.outListingFile)
const stagedListingJsonPath = context.prepareStagedFile(stagedDir, stagedFile, dstDir, dstFile)
await copyFile(srcListingJsonPath, stagedListingJsonPath)
}

const t1 = performance.now()
const elapsed = ((t1 - t0) / 1000).toFixed(1)
log('info', `Done generating model (${elapsed}s)`)
Expand Down
14 changes: 13 additions & 1 deletion packages/build/src/config/config-loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) 2022 Climate Interactive / New Venture Fund

import { existsSync, lstatSync, mkdirSync } from 'fs'
import { dirname, join as joinPath, relative, resolve as resolvePath } from 'path'
import { dirname, isAbsolute, join as joinPath, relative, resolve as resolvePath } from 'path'
import { fileURLToPath } from 'url'

import type { Result } from 'neverthrow'
Expand Down Expand Up @@ -171,6 +171,17 @@ function resolveUserConfig(
throw new Error(`The configured genFormat value is invalid; must be either 'js' or 'c'`)
}

// Validate the out listing file, if defined
let outListingFile: string
if (userConfig.outListingFile) {
// Get the absolute path of the output file
if (isAbsolute(userConfig.outListingFile)) {
outListingFile = userConfig.outListingFile
} else {
outListingFile = resolvePath(rootDir, userConfig.outListingFile)
}
}

return {
mode,
rootDir,
Expand All @@ -179,6 +190,7 @@ function resolveUserConfig(
modelInputPaths,
watchPaths,
genFormat,
outListingFile,
sdeDir,
sdeCmdPath
}
Expand Down
7 changes: 7 additions & 0 deletions packages/build/src/config/user-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export interface UserConfig {
*/
genFormat?: 'js' | 'c'

/**
* If defined, the build process will write a JSON file to the provided path that lists
* all dimensions and variables in the model. This can be an absolute path, or if it
* is a relative path it will be resolved relative to the `rootDir` for the project.
*/
outListingFile?: string

/**
* The array of plugins that are used to customize the build process. These will be
* executed in the order defined here.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// Copyright (c) 2022 Climate Interactive / New Venture Fund

import { resolve as resolvePath } from 'path'
import { rmSync } from 'node:fs'
import { resolve as resolvePath } from 'node:path'

import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it } from 'vitest'

import type { ModelSpec, UserConfig } from '../../src'
import { build } from '../../src'

import { buildOptions } from '../_shared/build-options'
import {} from 'vitest'

const modelSpec: ModelSpec = {
inputs: [{ varName: 'Y', defaultValue: 0, minValue: -10, maxValue: 10 }],
Expand All @@ -16,6 +18,11 @@ const modelSpec: ModelSpec = {
}

describe('build in development mode', () => {
beforeEach(() => {
const prepDir = resolvePath(__dirname, 'sde-prep')
rmSync(prepDir, { recursive: true, force: true })
})

it('should return undefined exit code', async () => {
const userConfig: UserConfig = {
rootDir: resolvePath(__dirname, '..'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Copyright (c) 2022 Climate Interactive / New Venture Fund

import { resolve as resolvePath } from 'path'
import { existsSync, rmSync } from 'node:fs'
import { join as joinPath, resolve as resolvePath } from 'node:path'

import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it } from 'vitest'

import type { ModelSpec, Plugin, UserConfig } from '../../src'
import { build } from '../../src'
Expand Down Expand Up @@ -56,6 +57,59 @@ const plugin = (num: number, calls: string[]) => {
}

describe('build in production mode', () => {
beforeEach(() => {
const prepDir = resolvePath(__dirname, 'sde-prep')
rmSync(prepDir, { recursive: true, force: true })

const outputsDir = resolvePath(__dirname, 'outputs')
rmSync(outputsDir, { recursive: true, force: true })
})

it('should write listing.json file (when absolute path is provided)', async () => {
const userConfig: UserConfig = {
genFormat: 'c',
rootDir: resolvePath(__dirname, '..'),
prepDir: resolvePath(__dirname, 'sde-prep'),
modelFiles: [resolvePath(__dirname, '..', '_shared', 'sample.mdl')],
// Note that `outListingFile` is specified with an absolute path here
outListingFile: resolvePath(__dirname, 'outputs', 'listing.json'),
modelSpec: async () => {
return modelSpec
}
}

const result = await build('production', buildOptions(userConfig))
if (result.isErr()) {
throw new Error('Expected ok result but got: ' + result.error.message)
}

expect(result.value.exitCode).toBe(0)
expect(existsSync(resolvePath(__dirname, 'outputs', 'listing.json'))).toBe(true)
})

it('should write listing.json file (when relative path is provided)', async () => {
const userConfig: UserConfig = {
genFormat: 'c',
rootDir: resolvePath(__dirname, '..'),
prepDir: resolvePath(__dirname, 'sde-prep'),
modelFiles: [resolvePath(__dirname, '..', '_shared', 'sample.mdl')],
// Note that `outListingFile` is specified with a relative path here, which
// will be resolved relative to `rootDir`
outListingFile: joinPath('build-prod', 'outputs', 'listing.json'),
modelSpec: async () => {
return modelSpec
}
}

const result = await build('production', buildOptions(userConfig))
if (result.isErr()) {
throw new Error('Expected ok result but got: ' + result.error.message)
}

expect(result.value.exitCode).toBe(0)
expect(existsSync(resolvePath(__dirname, 'outputs', 'listing.json'))).toBe(true)
})

it('should skip certain callbacks if model files array is empty', async () => {
const calls: string[] = []

Expand Down
10 changes: 8 additions & 2 deletions packages/build/tests/config/config.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
// Copyright (c) 2022 Climate Interactive / New Venture Fund

import { join as joinPath } from 'path'
import { rmSync } from 'node:fs'
import { join as joinPath } from 'node:path'

import { describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it } from 'vitest'

import { build } from '../../src'

import { buildOptions } from '../_shared/build-options'

describe('build config file loading', () => {
beforeEach(() => {
const prepDir = joinPath(__dirname, 'sde-prep')
rmSync(prepDir, { recursive: true, force: true })
})

it('should fail if config file cannot be found', async () => {
const configPath = joinPath(__dirname, 'sde.unknown.js')
const result = await build('production', buildOptions(configPath))
Expand Down