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: auto-create required function files #92

Merged
merged 16 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ jobs:
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: '/demo' # App source code path
api_location: 'demo/api' # Api source code path - optional
api_location: 'demo/build/server' # Api source code path - optional
output_location: 'build/static' # Built app content directory - optional
###### End of Repository/Build Configurations ######

Expand Down
68 changes: 45 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,14 @@ export default {
};
```

You will need to create an `api/` folder in your project root containing a [`host.json`](https://docs.microsoft.com/en-us/azure/azure-functions/functions-host-json) and a `package.json` (see samples below). The adapter will output the `render` Azure function for SSR in that folder. The `api` folder needs to be in your repo so that Azure can recognize the API at build time. However, you can add `api/render` to your .gitignore so that the generated function is not in source control.

### Sample `host.json`

```json
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[2.*, 3.0.0)"
}
}
```

### Sample `package.json`

It's okay for this to be empty. Not including it causes the Azure Function build to fail.

```json
{}
```

## Azure configuration

When deploying to Azure, you will need to properly [configure your build](https://docs.microsoft.com/en-us/azure/static-web-apps/build-configuration?tabs=github-actions) so that both the static files and API are deployed.

| property | value |
| ----------------- | -------------- |
| `app_location` | `./` |
| `api_location` | `api` |
| `api_location` | `build/server` |
| `output_location` | `build/static` |

## Running locally with the Azure SWA CLI
Expand All @@ -76,6 +54,50 @@ To run the CLI, install `@azure/static-web-apps-cli` and the [Azure Functions Co

## Options

### apiDir

The directory where the `render` Azure function for SSR will be placed. Most of the time, you shouldn't need to set this.

By default, the adapter will output the `render` Azure function for SSR in the `build/server` folder. If you want to output it to a different directory instead (e.g. if you have additional Azure functions to deploy), you can set this option.

```js
import azure from 'svelte-adapter-azure-swa';

export default {
kit: {
...
adapter: azure({
apiDir: 'custom/api'
geoffrich marked this conversation as resolved.
Show resolved Hide resolved
})
}
};
```

If you set this option, you will also need to create a `host.json` and `package.json` in your API directory. The adapter normally generates these files by default, but skips them when a custom API directory is provided to prevent overwriting any existing files. You can see the default files the adapter generates in [this directory](https://github.com/geoffrich/svelte-adapter-azure-swa/tree/main/files/api).

For instance, by default the adapter outputs these files...

```
build/
└── server/
├── render/
│ ├── function.json
│ └── index.js
├── host.json
├── local.settings.json
└── package.json
```

... but only outputs these files when a custom API directory is provided:

```
custom/
└── api/
└── render/
├── function.json
└── index.js
```

### customStaticWebAppConfig

An object containing additional Azure SWA [configuration options](https://docs.microsoft.com/en-us/azure/static-web-apps/configuration). This will be merged with the `staticwebapp.config.json` generated by the adapter.
Expand Down
1 change: 0 additions & 1 deletion demo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,3 @@ node_modules
!.env.example
.vercel
.output
/api/render
16 changes: 0 additions & 16 deletions files/api/function.json

This file was deleted.

File renamed without changes.
6 changes: 6 additions & 0 deletions files/api/local.settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"IsEncrypted": false,
"Values": {
"FUNCTIONS_WORKER_RUNTIME": "node"
}
}
File renamed without changes.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ type Options = {
debug?: boolean;
customStaticWebAppConfig?: CustomStaticWebAppConfig;
esbuildOptions?: Pick<esbuild.BuildOptions, 'external'>;
apiDir?: string;
};

export default function plugin(options?: Options): Adapter;
55 changes: 45 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,25 @@ import esbuild from 'esbuild';

const ssrFunctionRoute = '/api/__render';

const functionJson = `
{
"bindings": [
{
"authLevel": "anonymous",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "__render"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
`;

/**
* Validate the static web app configuration does not override the minimum config for the adapter to work correctly.
* @param config {import('./types/swa').CustomStaticWebAppConfig}
Expand All @@ -28,24 +47,20 @@ function validateCustomConfig(config) {
export default function ({
debug = false,
customStaticWebAppConfig = {},
esbuildOptions = {}
esbuildOptions = {},
apiDir: customApiDir = undefined
} = {}) {
return {
name: 'adapter-azure-swa',

async adapt(builder) {
if (!existsSync(join('api', 'package.json'))) {
throw new Error(
'You need to create a package.json in your `api` directory. See the adapter README for details.'
);
}

const swaConfig = generateConfig(customStaticWebAppConfig, builder.config.kit.appDir);

const tmp = builder.getBuildDirectory('azure-tmp');
const publish = 'build';
const staticDir = join(publish, 'static');
const apiDir = join('api', 'render');
const apiDir = customApiDir || join(publish, 'server');
const functionDir = join(apiDir, 'render');
const entry = `${tmp}/entry.js`;
builder.log.minor(`Publishing to "${publish}"`);

Expand All @@ -68,7 +83,12 @@ export default function ({
}
});

builder.copy(join(files, 'api'), apiDir);
if (customApiDir) {
checkForMissingFiles();
} else {
// if the user specified a custom API directory, assume they are creating the required function files themselves
builder.copy(join(files, 'api'), apiDir);
}

writeFileSync(
`${tmp}/manifest.js`,
Expand All @@ -80,7 +100,7 @@ export default function ({
/** @type {BuildOptions} */
const default_options = {
entryPoints: [entry],
outfile: join(apiDir, 'index.js'),
outfile: join(functionDir, 'index.js'),
bundle: true,
platform: 'node',
target: 'node16',
Expand All @@ -89,6 +109,7 @@ export default function ({
};

await esbuild.build(default_options);
writeFileSync(join(functionDir, 'function.json'), functionJson);

builder.log.minor('Copying assets...');
builder.writeClient(staticDir);
Expand All @@ -112,6 +133,20 @@ export default function ({
}

writeFileSync(`${publish}/staticwebapp.config.json`, JSON.stringify(swaConfig));

/**
* Check for missing files when a custom API directory is provided.
*/
function checkForMissingFiles() {
const requiredFiles = ['host.json', 'package.json'];
for (const file of requiredFiles) {
if (!existsSync(join(customApiDir, file))) {
builder.log.warn(
`Warning: apiDir set but ${file} does not exist. You will need to create this file yourself. See the docs for more information: https://github.com/geoffrich/svelte-adapter-azure-swa#apidir`
);
}
}
}
}
};
}
Expand Down
23 changes: 18 additions & 5 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,26 @@ describe('adapt', () => {
await adapter.adapt(builder);
expect(builder.writePrerendered).toBeCalled();
expect(builder.writeClient).toBeCalled();
expect(builder.copy).toBeCalledWith(expect.stringContaining('api'), 'build/server');
});

test('throws error when no package.json', async () => {
existsSync.mockImplementationOnce(() => false);
test('writes to custom api directory', async () => {
const adapter = azureAdapter({ apiDir: 'custom/api' });
const builder = getMockBuilder();
await adapter.adapt(builder);
expect(writeFileSync).toBeCalledWith(
'custom/api/render/function.json',
expect.stringContaining('__render')
);
expect(builder.copy).not.toBeCalledWith(expect.stringContaining('api'), 'custom/api');
});

const adapter = azureAdapter();
test('logs warning when custom api directory set and required file does not exist', async () => {
vi.mocked(existsSync).mockImplementationOnce(() => false);
const adapter = azureAdapter({ apiDir: 'custom/api' });
const builder = getMockBuilder();
await expect(adapter.adapt(builder)).rejects.toThrowError('You need to create a package.json');
await adapter.adapt(builder);
expect(builder.log.warn).toBeCalled();
});

test('adds index.html when root not prerendered', async () => {
Expand Down Expand Up @@ -112,7 +124,8 @@ function getMockBuilder() {
}
},
log: {
minor: vi.fn()
minor: vi.fn(),
warn: vi.fn()
},
prerendered: {
paths: ['/']
Expand Down