Skip to content

Commit

Permalink
feat: auto-create required function files (#92)
Browse files Browse the repository at this point in the history
* feat: auto-create required function files

* feat: auto-generate local.settings.json

* refactor: write function.json programatically

* feat: support custom API directory

* feat: warn when required files are missing

* docs: document new options

* fix: don't call rimraf on apiDir

* chore: fix deploy

* Fix E2E test

Not sure why this broke, but the test input isn't taking on CI. Focusing the input first fixes it.

* refactor: output to server/__render

* refactor: use /sk_render directory

* docs: update apiDir docs

* feat: add warning if api/render exists

* docs: add warning about build config
  • Loading branch information
geoffrich committed Nov 21, 2022
1 parent e381aec commit c682164
Show file tree
Hide file tree
Showing 11 changed files with 131 additions and 56 deletions.
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 ######
outputs:
Expand Down
74 changes: 51 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,27 +23,7 @@ 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
{}
```
:warning: **IMPORTANT**: you also need to configure your build so that your SvelteKit site deploys properly. Failing to do so will prevent the project from building and deploying. See the next section for instructions.

## Azure configuration

Expand All @@ -52,9 +32,11 @@ When deploying to Azure, you will need to properly [configure your build](https:
| property | value |
| ----------------- | -------------- |
| `app_location` | `./` |
| `api_location` | `api` |
| `api_location` | `build/server` |
| `output_location` | `build/static` |

If you use a custom API directory (see [below](#apiDir)), your `api_location` will be the same as the value you pass to `apiDir`.

## Running locally with the Azure SWA CLI

You can debug using the [Azure Static Web Apps CLI](https://github.com/Azure/static-web-apps-cli). Note that the CLI is currently in preview and you may encounter issues.
Expand All @@ -68,14 +50,60 @@ To run the CLI, install `@azure/static-web-apps-cli` and the [Azure Functions Co
"configurations": {
"app": {
"outputLocation": "./build/static",
"apiLocation": "./api"
"apiLocation": "./build/server"
}
}
}
```

## Options

### apiDir

The directory where the `sk_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 `sk_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'
})
}
};
```

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/
├── sk_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/
└── sk_render/
├── function.json
└── index.js
```

Also note that the adapter reserves the folder prefix `sk_render` and API route prefix `__render` for Azure functions generated by the adapter. So, if you use a custom API directory, you cannot have any other folder starting with `sk_render` or functions available at the `__render` route, since these will conflict with the adapter's Azure functions.

### 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
2 changes: 2 additions & 0 deletions demo/tests/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ test('submits sverdle guess', async ({ page }) => {
await page.goto('/sverdle');
const input = page.locator('input[name=guess]').first();
await expect(input).not.toBeDisabled();
await input.focus();

await page.keyboard.type('AZURE');
await page.keyboard.press('Enter');

await expect(input).toHaveValue('a');
await expect(input).toBeDisabled();
});
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;
61 changes: 51 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,30 +47,32 @@ 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.'
// TODO: remove for 1.0
if (!customApiDir && existsSync(join('api', 'render'))) {
builder.log.warn(
`Warning: you have an api/render folder but this adapter now uses the build/server folder for API functions. You may need to update your build configuration. Failing to do so could break your deployed site.
Please see the PR for migration instructions: https://github.com/geoffrich/svelte-adapter-azure-swa/pull/92`
);
}

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, 'sk_render');
const entry = `${tmp}/entry.js`;
builder.log.minor(`Publishing to "${publish}"`);

builder.rimraf(tmp);
builder.rimraf(publish);
builder.rimraf(apiDir);

const files = fileURLToPath(new URL('./files', import.meta.url));

Expand All @@ -68,7 +89,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 +106,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 +115,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 @@ -111,7 +138,21 @@ export default function ({
);
}

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

/**
* 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
24 changes: 19 additions & 5 deletions test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,27 @@ 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/sk_render/function.json',
expect.stringContaining('__render')
);
// we don't copy the required function files to a custom API directory
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 +125,8 @@ function getMockBuilder() {
}
},
log: {
minor: vi.fn()
minor: vi.fn(),
warn: vi.fn()
},
prerendered: {
paths: ['/']
Expand Down

0 comments on commit c682164

Please sign in to comment.